From 12ea6aeaf2556a30b4655a887125e0eb8594fd73 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:21:32 +0530 Subject: [PATCH 01/24] implemented Group Option Name Features: * all Non-Blank (based on full whitespace) Group Names are shown as-is in Usage * all Blank Group Names get a count-based default Group Name within Usage * all Groupless Options are shown before Grouped Options * relative order of all Options within a Group is preserved * relative order of all Groups is preserved --- packages/config/lib/src/config/options.dart | 55 ++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/packages/config/lib/src/config/options.dart b/packages/config/lib/src/config/options.dart index e4d4e15..d0e4150 100644 --- a/packages/config/lib/src/config/options.dart +++ b/packages/config/lib/src/config/options.dart @@ -804,9 +804,62 @@ void addOptionsToParser( final Iterable argNameOpts, final ArgParser argParser, ) { + // Gather all necessary Option Group information + final optionGroups = >{}; // ordered map + final grouplessOptions = []; // ordered list for (final opt in argNameOpts) { - opt.option._addToArgParser(argParser); + final group = opt.option.group; + if (group != null) { + optionGroups.update( + group, + (final value) => [...value, opt], + ifAbsent: () => [opt], + ); + } else { + grouplessOptions.add(opt); + } } + final nOptionGroups = optionGroups.keys.length; + + // Helpers for consistent processing and validation + const defaultFallbackGroupName = 'Option Group'; + void addOne(final OptionDefinition x) => x.option._addToArgParser(argParser); + void addAll(final List options) => options.forEach(addOne); + bool isNotBlank(final String name) => name.trim().isNotEmpty; + String buildFallbackGroupName(int groupCounter) => + '$defaultFallbackGroupName${nOptionGroups > 1 ? " $groupCounter" : ""}'; + + // Add all Groupless Options first (in order) + addAll(grouplessOptions); + + // Add all Groups (in order) and their Options (in order) + var groupCounter = 0; + optionGroups.forEach((final group, final options) { + ++groupCounter; + var givenGroupName = group.name; + var fallbackGroupName = buildFallbackGroupName(groupCounter); + + // IMPORTANT NOTE for this If-Block: + // - resolves some bug which fails newline padding in this particular case + // - the bug was probably bubbled down from the external Args Package + // - this code MUST BE REMOVED WHEN THIS BUG IS RESOLVED + // - when this block is removed, both the Group Names can be `final`. + // + // Unit Tests for Group Usage Text covers this case, so + // it shall serve as a reminder by failing when this bug gets patched. + if (grouplessOptions.isEmpty && groupCounter == 1) { + givenGroupName = '$givenGroupName\n'; + fallbackGroupName = '$fallbackGroupName\n'; + } + + // Add the Group Name after handling potential Blank Names + argParser.addSeparator( + isNotBlank(givenGroupName) ? givenGroupName : fallbackGroupName, + ); + + // Add all the Grouped Options (in order) + addAll(options); + }); } extension PrepareOptions on Iterable { From 31a938ed22160582868d01c020ebe912028c1247 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:29:04 +0530 Subject: [PATCH 02/24] added Unit Tests for Group Name Objective: * All Group Names are properly padded with newlines in the presence of Groupless Options * All Group Names are properly padded with newlines in the absence of Groupless Options * Blank Group Names are not rendered as-is in the presence of Groupless Options * Blank Group Names are not rendered as-is in the absence of Groupless Options * Non-Blank Group Names are rendered as-is in the presence of Groupless Options * Non-Blank Group Names are rendered as-is in the absence of Groupless Options * Blank Group Names get a count-based default Group Name in the presence of Groupless Options * Blank Group Names get a count-based default Group Name in the absence of Groupless Options * All Groupless Options are shown before Grouped Options * Relative order of all Options within a Group is preserved * Relative order of all Groups is preserved --- .../option_group_usage_text_test.dart | 358 ++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart diff --git a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart new file mode 100644 index 0000000..e25c5c7 --- /dev/null +++ b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart @@ -0,0 +1,358 @@ +import 'package:cli_tools/better_command_runner.dart'; +import 'package:config/config.dart'; +import 'package:test/test.dart'; + +void main() { + const mockCommandName = 'mock'; + const mockCommandDescription = 'This is a Mock CLI'; + const fallbackGroupName = 'Option Group'; + const blankNameExamples = [ + '', + ' ', + ' ', + ' ', + '\n', + '\n\n', + '\n\n\n', + '\t', + '\t\t', + '\t\t\t', + ]; + final nBlankNameExamples = blankNameExamples.length; + + String buildSeparatorView(final String name) => '\n$name\n\n'; + + String buildSuffix([final Object? suffix = '', final String prefix = '']) => + suffix != null && suffix != '' ? '$prefix${suffix.toString()}' : ''; + + String buildMockArgName([final Object? suffix = '']) => + 'mock-arg${buildSuffix(suffix, '-')}'; + + String buildMockGroupName([final Object? suffix = '']) => + 'Mock Group${buildSuffix(suffix, ' ')}'; + + String buildFallbackGroupName([final Object? suffix = '']) => + '$fallbackGroupName${buildSuffix(suffix, ' ')}'; + + group('All Group Names are properly padded with newlines', () { + test( + 'in the presence of Groupless Options', + () { + expect( + BetterCommandRunner( + mockCommandName, + mockCommandDescription, + globalOptions: [ + for (var i = 0; i < 5; ++i) + FlagOption( + argName: buildMockArgName(i), + group: OptionGroup(buildMockGroupName(i)), + ), + for (var i = 5; i < 10; ++i) + FlagOption( + argName: buildMockArgName(i), + ), + ], + ).usage, + allOf([ + for (var i = 0; i < 5; ++i) + contains(buildSeparatorView(buildMockGroupName(i))), + ]), + ); + }, + ); + test( + 'in the absence of Groupless Options', + () { + expect( + BetterCommandRunner( + mockCommandName, + mockCommandDescription, + globalOptions: [ + for (var i = 0; i < 5; ++i) + FlagOption( + argName: buildMockArgName(i), + group: OptionGroup(buildMockGroupName(i)), + ), + ], + ).usage, + allOf([ + for (var i = 0; i < 5; ++i) + contains(buildSeparatorView(buildMockGroupName(i))), + ]), + ); + }, + ); + }); + + group('Blank Group Names are not rendered as-is', () { + test( + 'in the presence of Groupless Options', + () { + expect( + BetterCommandRunner( + mockCommandName, + mockCommandDescription, + globalOptions: [ + for (var i = 0; i < nBlankNameExamples; ++i) + FlagOption( + argName: buildMockArgName(i), + group: OptionGroup(blankNameExamples[i]), + ), + for (var i = nBlankNameExamples; i < nBlankNameExamples + 5; ++i) + FlagOption( + argName: buildMockArgName(i), + ), + ], + ).usage, + allOf([ + for (final blankName in blankNameExamples) + isNot(contains(buildSeparatorView(blankName))), + ]), + ); + }, + ); + test( + 'in the absence of Groupless Options', + () { + expect( + BetterCommandRunner( + mockCommandName, + mockCommandDescription, + globalOptions: [ + for (var i = 0; i < nBlankNameExamples; ++i) + FlagOption( + argName: buildMockArgName(i), + group: OptionGroup(blankNameExamples[i]), + ), + ], + ).usage, + allOf([ + for (final blankName in blankNameExamples) + isNot(contains(buildSeparatorView(blankName))), + ]), + ); + }, + ); + }); + + group('Non-Blank Group Names are rendered as-is', () { + test( + 'in the presence of Groupless Options', + () { + expect( + BetterCommandRunner( + mockCommandName, + mockCommandDescription, + globalOptions: [ + for (var i = 0; i < 5; ++i) + FlagOption( + argName: buildMockArgName(i), + group: OptionGroup(buildMockGroupName(i)), + ), + for (var i = 5; i < 10; ++i) + FlagOption( + argName: buildMockArgName(i), + ), + ], + ).usage, + allOf([ + for (var i = 0; i < 5; ++i) + contains(buildSeparatorView(buildMockGroupName(i))), + ]), + ); + }, + ); + test( + 'in the absence of Groupless Options', + () { + expect( + BetterCommandRunner( + mockCommandName, + mockCommandDescription, + globalOptions: [ + for (var i = 0; i < 5; ++i) + FlagOption( + argName: buildMockArgName(i), + group: OptionGroup(buildMockGroupName(i)), + ), + ], + ).usage, + allOf([ + for (var i = 0; i < 5; ++i) + contains(buildSeparatorView(buildMockGroupName(i))), + ]), + ); + }, + ); + }); + + group('Blank Group Names get a count-based default Group Name', () { + test( + 'in the presence of Groupless Options', + () { + expect( + BetterCommandRunner( + mockCommandName, + mockCommandDescription, + globalOptions: [ + FlagOption( + argName: buildMockArgName(1), + group: OptionGroup(blankNameExamples.first), + ), + FlagOption( + argName: buildMockArgName(2), + group: OptionGroup(buildMockGroupName('XYZ')), + ), + FlagOption( + argName: buildMockArgName(3), + group: OptionGroup(blankNameExamples.last), + ), + for (var i = 100; i < 105; ++i) + FlagOption( + argName: buildMockArgName(i), + ), + ], + ).usage, + stringContainsInOrder([ + buildSeparatorView(buildFallbackGroupName(1)), + buildSeparatorView(buildMockGroupName('XYZ')), + buildSeparatorView(buildFallbackGroupName(3)), + ]), + ); + }, + ); + test( + 'in the absence of Groupless Options', + () { + expect( + BetterCommandRunner( + mockCommandName, + mockCommandDescription, + globalOptions: [ + FlagOption( + argName: buildMockArgName(1), + group: OptionGroup(blankNameExamples.first), + ), + FlagOption( + argName: buildMockArgName(2), + group: OptionGroup(buildMockGroupName('XYZ')), + ), + FlagOption( + argName: buildMockArgName(3), + group: OptionGroup(blankNameExamples.last), + ), + ], + ).usage, + stringContainsInOrder([ + buildSeparatorView(buildFallbackGroupName(1)), + buildSeparatorView(buildMockGroupName('XYZ')), + buildSeparatorView(buildFallbackGroupName(3)), + ]), + ); + }, + ); + }); + + test( + 'All Groupless Options are shown before Grouped Options', + () { + expect( + BetterCommandRunner( + mockCommandName, + mockCommandDescription, + globalOptions: [ + FlagOption( + argName: buildMockArgName(1), + group: OptionGroup(blankNameExamples.first), + ), + FlagOption( + argName: buildMockArgName(2), + group: OptionGroup(buildMockGroupName('XYZ')), + ), + FlagOption( + argName: buildMockArgName(3), + group: OptionGroup(blankNameExamples.last), + ), + for (var i = 100; i < 105; ++i) + FlagOption( + argName: buildMockArgName(i), + ), + ], + ).usage, + stringContainsInOrder([ + '\n', + for (var i = 100; i < 105; ++i) buildMockArgName(i), + '\n', + buildSeparatorView(buildFallbackGroupName(1)), + buildSeparatorView(buildMockGroupName('XYZ')), + buildSeparatorView(buildFallbackGroupName(3)), + ]), + ); + }, + ); + + test( + 'Relative order of all Options within a Group is preserved', + () { + var optionCount1 = 0; + var optionCount2 = 0; + expect( + BetterCommandRunner( + mockCommandName, + mockCommandDescription, + globalOptions: [ + for (var i = 0; i < 5; ++i) + FlagOption( + argName: buildMockArgName(++optionCount1), + ), + for (var i = 0; i < 3; ++i) + for (var j = 0; j < 5; ++j) + FlagOption( + argName: buildMockArgName(++optionCount1), + group: OptionGroup(buildMockGroupName(i)), + ), + ], + ).usage, + stringContainsInOrder([ + '\n', + for (var i = 0; i < 5; ++i) buildMockArgName(++optionCount2), + '\n', + for (var i = 0; i < 3; ++i) + for (var j = 0; j < 5; ++j) buildMockArgName(++optionCount2), + ]), + ); + }, + ); + + test( + 'Relative order of all Groups is preserved', + () { + var optionCount = 0; + var groupCount1 = 0; + var groupCount2 = 0; + expect( + BetterCommandRunner( + mockCommandName, + mockCommandDescription, + globalOptions: [ + for (var i = 0; i < 5; ++i) + FlagOption( + argName: buildMockArgName(++optionCount), + ), + for (var i = 0; i < 3; ++i) + for (var j = 0; j < 5; ++j) + FlagOption( + argName: buildMockArgName(++optionCount), + group: OptionGroup(buildMockGroupName(++groupCount1)), + ), + ], + ).usage, + stringContainsInOrder([ + for (var i = 0; i < groupCount1; ++i) + buildSeparatorView(buildMockGroupName(++groupCount2)), + ]), + ); + }, + ); +} From aaa430eb07a634e6f4260d8e768239ae7dce14ea Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Wed, 8 Oct 2025 00:59:32 +0530 Subject: [PATCH 03/24] style: Consistent usage of `final` To match the existing Code Style. --- packages/config/lib/src/config/options.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/config/lib/src/config/options.dart b/packages/config/lib/src/config/options.dart index d0e4150..b391048 100644 --- a/packages/config/lib/src/config/options.dart +++ b/packages/config/lib/src/config/options.dart @@ -826,7 +826,7 @@ void addOptionsToParser( void addOne(final OptionDefinition x) => x.option._addToArgParser(argParser); void addAll(final List options) => options.forEach(addOne); bool isNotBlank(final String name) => name.trim().isNotEmpty; - String buildFallbackGroupName(int groupCounter) => + String buildFallbackGroupName(final int groupCounter) => '$defaultFallbackGroupName${nOptionGroups > 1 ? " $groupCounter" : ""}'; // Add all Groupless Options first (in order) From c9ed4a4ab072fad0e3065f182ad6641bb02336df Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Wed, 8 Oct 2025 01:10:35 +0530 Subject: [PATCH 04/24] perf: Minimize condition-checks AND-condition Optimization. --- packages/config/lib/src/config/options.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/config/lib/src/config/options.dart b/packages/config/lib/src/config/options.dart index b391048..22cf6a2 100644 --- a/packages/config/lib/src/config/options.dart +++ b/packages/config/lib/src/config/options.dart @@ -847,7 +847,7 @@ void addOptionsToParser( // // Unit Tests for Group Usage Text covers this case, so // it shall serve as a reminder by failing when this bug gets patched. - if (grouplessOptions.isEmpty && groupCounter == 1) { + if (groupCounter == 1 && grouplessOptions.isEmpty) { givenGroupName = '$givenGroupName\n'; fallbackGroupName = '$fallbackGroupName\n'; } From 6146a2eb7ab7ff039863988986f3edf3e2e25835 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Wed, 8 Oct 2025 01:52:06 +0530 Subject: [PATCH 05/24] perf: Temporary reallocation instances avoided via Cascade Operator As suggested (nitpick) by the Review Bot on my Pull Request. --- packages/config/lib/src/config/options.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/config/lib/src/config/options.dart b/packages/config/lib/src/config/options.dart index 22cf6a2..4ef741a 100644 --- a/packages/config/lib/src/config/options.dart +++ b/packages/config/lib/src/config/options.dart @@ -812,7 +812,7 @@ void addOptionsToParser( if (group != null) { optionGroups.update( group, - (final value) => [...value, opt], + (final options) => options..add(opt), ifAbsent: () => [opt], ); } else { From a5561b734ee361d1249054c14f6001320250282f Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Wed, 8 Oct 2025 02:05:31 +0530 Subject: [PATCH 06/24] refactor: Satisfy the nitpicky Review Bot Anyways, I reckon it's a good improvement lol --- packages/config/lib/src/config/options.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/config/lib/src/config/options.dart b/packages/config/lib/src/config/options.dart index 4ef741a..30c52ba 100644 --- a/packages/config/lib/src/config/options.dart +++ b/packages/config/lib/src/config/options.dart @@ -819,15 +819,14 @@ void addOptionsToParser( grouplessOptions.add(opt); } } - final nOptionGroups = optionGroups.keys.length; // Helpers for consistent processing and validation const defaultFallbackGroupName = 'Option Group'; void addOne(final OptionDefinition x) => x.option._addToArgParser(argParser); void addAll(final List options) => options.forEach(addOne); bool isNotBlank(final String name) => name.trim().isNotEmpty; - String buildFallbackGroupName(final int groupCounter) => - '$defaultFallbackGroupName${nOptionGroups > 1 ? " $groupCounter" : ""}'; + String buildFallbackGroupName(final int counter) => + '$defaultFallbackGroupName${optionGroups.length > 1 ? " $counter" : ""}'; // Add all Groupless Options first (in order) addAll(grouplessOptions); From abba30ecf0baa2659970c2b10c72cdab5150bd10 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Wed, 8 Oct 2025 12:03:45 +0530 Subject: [PATCH 07/24] refactor (potential fix): Backwards Compatibility A flag to choose between the previous behavior of `addOptionsToParser` (default to not affect the existing Codebase) and the new implementation that handles Group Option Name logic. --- packages/config/lib/src/config/config_parser.dart | 2 +- packages/config/lib/src/config/options.dart | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/config/lib/src/config/config_parser.dart b/packages/config/lib/src/config/config_parser.dart index dc74e5c..0c34102 100644 --- a/packages/config/lib/src/config/config_parser.dart +++ b/packages/config/lib/src/config/config_parser.dart @@ -210,7 +210,7 @@ class ConfigParser implements ArgParser { void _addOption(final OptionDefinition opt) { _optionDefinitions.add(opt); // added continuously to the parser so separators are placed correctly: - addOptionsToParser([opt], _parser); + addOptionsToParser([opt], _parser, addGroupSeparators: false); } @override diff --git a/packages/config/lib/src/config/options.dart b/packages/config/lib/src/config/options.dart index 30c52ba..54a4085 100644 --- a/packages/config/lib/src/config/options.dart +++ b/packages/config/lib/src/config/options.dart @@ -722,7 +722,7 @@ void prepareOptionsForParsing( final ArgParser argParser, ) { final argNameOpts = validateOptions(options); - addOptionsToParser(argNameOpts, argParser); + addOptionsToParser(argNameOpts, argParser, addGroupSeparators: true); } Iterable validateOptions( @@ -802,8 +802,17 @@ Iterable validateOptions( void addOptionsToParser( final Iterable argNameOpts, - final ArgParser argParser, -) { + final ArgParser argParser, { + final bool addGroupSeparators = false, +}) { + // Plain Option-addition without any Group Logic + if (!addGroupSeparators) { + for (final opt in argNameOpts) { + opt.option._addToArgParser(argParser); + } + return; + } + // Gather all necessary Option Group information final optionGroups = >{}; // ordered map final grouplessOptions = []; // ordered list From 48e440bf8c85248e47cbfe8e7bbcf5ae74820503 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Wed, 8 Oct 2025 12:23:08 +0530 Subject: [PATCH 08/24] refactor: Choice to customize default Group Name Addresses the Review Bot's nitpick comment on `defaultFallbackGroupName` in a better way. --- packages/config/lib/src/config/options.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/config/lib/src/config/options.dart b/packages/config/lib/src/config/options.dart index 54a4085..2b26c65 100644 --- a/packages/config/lib/src/config/options.dart +++ b/packages/config/lib/src/config/options.dart @@ -804,6 +804,7 @@ void addOptionsToParser( final Iterable argNameOpts, final ArgParser argParser, { final bool addGroupSeparators = false, + final String defaultFallbackGroupName = 'Option Group', }) { // Plain Option-addition without any Group Logic if (!addGroupSeparators) { @@ -830,7 +831,6 @@ void addOptionsToParser( } // Helpers for consistent processing and validation - const defaultFallbackGroupName = 'Option Group'; void addOne(final OptionDefinition x) => x.option._addToArgParser(argParser); void addAll(final List options) => options.forEach(addOne); bool isNotBlank(final String name) => name.trim().isNotEmpty; From 2eaea17102d8ad4a101663d677671a07b69ea644 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Thu, 9 Oct 2025 02:38:55 +0530 Subject: [PATCH 09/24] fix: Upstream Bug detected and escalated I have raised [an Issue](https://github.com/dart-lang/core/issues/917) for `package:args` and updated this Codebase with the expected behavior and adjusted (+refactored) the Test Cases accordingly. --- .../option_group_usage_text_test.dart | 148 ++++++++---------- packages/config/lib/src/config/options.dart | 17 +- 2 files changed, 71 insertions(+), 94 deletions(-) diff --git a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart index e25c5c7..aa72611 100644 --- a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart +++ b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart @@ -5,6 +5,7 @@ import 'package:test/test.dart'; void main() { const mockCommandName = 'mock'; const mockCommandDescription = 'This is a Mock CLI'; + const mockHelpText = 'Mock Help Text.'; const fallbackGroupName = 'Option Group'; const blankNameExamples = [ '', @@ -20,7 +21,7 @@ void main() { ]; final nBlankNameExamples = blankNameExamples.length; - String buildSeparatorView(final String name) => '\n$name\n\n'; + String buildSeparatorView(final String name) => '\n$name\n'; String buildSuffix([final Object? suffix = '', final String prefix = '']) => suffix != null && suffix != '' ? '$prefix${suffix.toString()}' : ''; @@ -34,6 +35,19 @@ void main() { String buildFallbackGroupName([final Object? suffix = '']) => '$fallbackGroupName${buildSuffix(suffix, ' ')}'; + OptionGroup buildMockGroup([final Object? suffix = '']) => + OptionGroup(buildMockGroupName(suffix)); + + OptionDefinition buildMockOption( + final String name, + final OptionGroup? group, + ) => + FlagOption( + argName: name, + group: group, + helpText: mockHelpText, + ); + group('All Group Names are properly padded with newlines', () { test( 'in the presence of Groupless Options', @@ -44,14 +58,9 @@ void main() { mockCommandDescription, globalOptions: [ for (var i = 0; i < 5; ++i) - FlagOption( - argName: buildMockArgName(i), - group: OptionGroup(buildMockGroupName(i)), - ), + buildMockOption(buildMockArgName(i), buildMockGroup(i)), for (var i = 5; i < 10; ++i) - FlagOption( - argName: buildMockArgName(i), - ), + buildMockOption(buildMockArgName(i), null), ], ).usage, allOf([ @@ -70,10 +79,7 @@ void main() { mockCommandDescription, globalOptions: [ for (var i = 0; i < 5; ++i) - FlagOption( - argName: buildMockArgName(i), - group: OptionGroup(buildMockGroupName(i)), - ), + buildMockOption(buildMockArgName(i), buildMockGroup(i)), ], ).usage, allOf([ @@ -95,19 +101,18 @@ void main() { mockCommandDescription, globalOptions: [ for (var i = 0; i < nBlankNameExamples; ++i) - FlagOption( - argName: buildMockArgName(i), - group: OptionGroup(blankNameExamples[i]), + buildMockOption( + buildMockArgName(i), + OptionGroup(blankNameExamples[i]), ), for (var i = nBlankNameExamples; i < nBlankNameExamples + 5; ++i) - FlagOption( - argName: buildMockArgName(i), - ), + buildMockOption(buildMockArgName(i), null), ], ).usage, allOf([ for (final blankName in blankNameExamples) - isNot(contains(buildSeparatorView(blankName))), + if (blankName != '' && blankName != '\n') + isNot(contains(buildSeparatorView(blankName))), ]), ); }, @@ -121,15 +126,16 @@ void main() { mockCommandDescription, globalOptions: [ for (var i = 0; i < nBlankNameExamples; ++i) - FlagOption( - argName: buildMockArgName(i), - group: OptionGroup(blankNameExamples[i]), + buildMockOption( + buildMockArgName(i), + OptionGroup(blankNameExamples[i]), ), ], ).usage, allOf([ for (final blankName in blankNameExamples) - isNot(contains(buildSeparatorView(blankName))), + if (blankName != '' && blankName != '\n') + isNot(contains(buildSeparatorView(blankName))), ]), ); }, @@ -146,14 +152,9 @@ void main() { mockCommandDescription, globalOptions: [ for (var i = 0; i < 5; ++i) - FlagOption( - argName: buildMockArgName(i), - group: OptionGroup(buildMockGroupName(i)), - ), + buildMockOption(buildMockArgName(i), buildMockGroup(i)), for (var i = 5; i < 10; ++i) - FlagOption( - argName: buildMockArgName(i), - ), + buildMockOption(buildMockArgName(i), null), ], ).usage, allOf([ @@ -172,10 +173,7 @@ void main() { mockCommandDescription, globalOptions: [ for (var i = 0; i < 5; ++i) - FlagOption( - argName: buildMockArgName(i), - group: OptionGroup(buildMockGroupName(i)), - ), + buildMockOption(buildMockArgName(i), buildMockGroup(i)), ], ).usage, allOf([ @@ -196,22 +194,20 @@ void main() { mockCommandName, mockCommandDescription, globalOptions: [ - FlagOption( - argName: buildMockArgName(1), - group: OptionGroup(blankNameExamples.first), + buildMockOption( + buildMockArgName(1), + OptionGroup(blankNameExamples.first), ), - FlagOption( - argName: buildMockArgName(2), - group: OptionGroup(buildMockGroupName('XYZ')), + buildMockOption( + buildMockArgName(2), + OptionGroup(buildMockGroupName('XYZ')), ), - FlagOption( - argName: buildMockArgName(3), - group: OptionGroup(blankNameExamples.last), + buildMockOption( + buildMockArgName(3), + OptionGroup(blankNameExamples.last), ), for (var i = 100; i < 105; ++i) - FlagOption( - argName: buildMockArgName(i), - ), + buildMockOption(buildMockArgName(i), null), ], ).usage, stringContainsInOrder([ @@ -230,17 +226,17 @@ void main() { mockCommandName, mockCommandDescription, globalOptions: [ - FlagOption( - argName: buildMockArgName(1), - group: OptionGroup(blankNameExamples.first), + buildMockOption( + buildMockArgName(1), + OptionGroup(blankNameExamples.first), ), - FlagOption( - argName: buildMockArgName(2), - group: OptionGroup(buildMockGroupName('XYZ')), + buildMockOption( + buildMockArgName(2), + OptionGroup(buildMockGroupName('XYZ')), ), - FlagOption( - argName: buildMockArgName(3), - group: OptionGroup(blankNameExamples.last), + buildMockOption( + buildMockArgName(3), + OptionGroup(blankNameExamples.last), ), ], ).usage, @@ -262,22 +258,20 @@ void main() { mockCommandName, mockCommandDescription, globalOptions: [ - FlagOption( - argName: buildMockArgName(1), - group: OptionGroup(blankNameExamples.first), + buildMockOption( + buildMockArgName(1), + OptionGroup(blankNameExamples.first), ), - FlagOption( - argName: buildMockArgName(2), - group: OptionGroup(buildMockGroupName('XYZ')), + buildMockOption( + buildMockArgName(2), + OptionGroup(buildMockGroupName('XYZ')), ), - FlagOption( - argName: buildMockArgName(3), - group: OptionGroup(blankNameExamples.last), + buildMockOption( + buildMockArgName(3), + OptionGroup(blankNameExamples.last), ), for (var i = 100; i < 105; ++i) - FlagOption( - argName: buildMockArgName(i), - ), + buildMockOption(buildMockArgName(i), null), ], ).usage, stringContainsInOrder([ @@ -303,14 +297,12 @@ void main() { mockCommandDescription, globalOptions: [ for (var i = 0; i < 5; ++i) - FlagOption( - argName: buildMockArgName(++optionCount1), - ), + buildMockOption(buildMockArgName(++optionCount1), null), for (var i = 0; i < 3; ++i) for (var j = 0; j < 5; ++j) - FlagOption( - argName: buildMockArgName(++optionCount1), - group: OptionGroup(buildMockGroupName(i)), + buildMockOption( + buildMockArgName(++optionCount1), + OptionGroup(buildMockGroupName(i)), ), ], ).usage, @@ -337,14 +329,12 @@ void main() { mockCommandDescription, globalOptions: [ for (var i = 0; i < 5; ++i) - FlagOption( - argName: buildMockArgName(++optionCount), - ), + buildMockOption(buildMockArgName(++optionCount), null), for (var i = 0; i < 3; ++i) for (var j = 0; j < 5; ++j) - FlagOption( - argName: buildMockArgName(++optionCount), - group: OptionGroup(buildMockGroupName(++groupCount1)), + buildMockOption( + buildMockArgName(++optionCount), + buildMockGroup(++groupCount1), ), ], ).usage, diff --git a/packages/config/lib/src/config/options.dart b/packages/config/lib/src/config/options.dart index 2b26c65..dcf127c 100644 --- a/packages/config/lib/src/config/options.dart +++ b/packages/config/lib/src/config/options.dart @@ -844,21 +844,8 @@ void addOptionsToParser( var groupCounter = 0; optionGroups.forEach((final group, final options) { ++groupCounter; - var givenGroupName = group.name; - var fallbackGroupName = buildFallbackGroupName(groupCounter); - - // IMPORTANT NOTE for this If-Block: - // - resolves some bug which fails newline padding in this particular case - // - the bug was probably bubbled down from the external Args Package - // - this code MUST BE REMOVED WHEN THIS BUG IS RESOLVED - // - when this block is removed, both the Group Names can be `final`. - // - // Unit Tests for Group Usage Text covers this case, so - // it shall serve as a reminder by failing when this bug gets patched. - if (groupCounter == 1 && grouplessOptions.isEmpty) { - givenGroupName = '$givenGroupName\n'; - fallbackGroupName = '$fallbackGroupName\n'; - } + final givenGroupName = group.name; + final fallbackGroupName = buildFallbackGroupName(groupCounter); // Add the Group Name after handling potential Blank Names argParser.addSeparator( From 67c1c7ad81b00c75d2b2f66bcd243bfbb4274c78 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:31:31 +0530 Subject: [PATCH 10/24] test: Improvement to `option_group_usage_text_test.dart` - more rigorous (e.g. newline precision and order) - more readable (e.g. highly modular) - easily enhanceable (e.g. via almost direct copy-paste for similar and additional Test Cases) --- .../option_group_usage_text_test.dart | 380 ++++++++---------- 1 file changed, 164 insertions(+), 216 deletions(-) diff --git a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart index aa72611..b163d25 100644 --- a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart +++ b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart @@ -4,9 +4,9 @@ import 'package:test/test.dart'; void main() { const mockCommandName = 'mock'; - const mockCommandDescription = 'This is a Mock CLI'; - const mockHelpText = 'Mock Help Text.'; - const fallbackGroupName = 'Option Group'; + const mockCommandDescription = 'A mock CLI for Option Group Usage Text test.'; + const mockOptionHelpText = 'Help Text for this Mock Option.'; + const defaultFallbackGroupName = 'Option Group'; const blankNameExamples = [ '', ' ', @@ -19,9 +19,8 @@ void main() { '\t\t', '\t\t\t', ]; - final nBlankNameExamples = blankNameExamples.length; - String buildSeparatorView(final String name) => '\n$name\n'; + String buildSeparatorView(final String name) => '\n\n$name\n'; String buildSuffix([final Object? suffix = '', final String prefix = '']) => suffix != null && suffix != '' ? '$prefix${suffix.toString()}' : ''; @@ -33,40 +32,45 @@ void main() { 'Mock Group${buildSuffix(suffix, ' ')}'; String buildFallbackGroupName([final Object? suffix = '']) => - '$fallbackGroupName${buildSuffix(suffix, ' ')}'; + '$defaultFallbackGroupName${buildSuffix(suffix, ' ')}'; OptionGroup buildMockGroup([final Object? suffix = '']) => OptionGroup(buildMockGroupName(suffix)); OptionDefinition buildMockOption( - final String name, + final String name, [ final OptionGroup? group, - ) => + ]) => FlagOption( argName: name, group: group, - helpText: mockHelpText, + helpText: mockOptionHelpText, ); - group('All Group Names are properly padded with newlines', () { + BetterCommandRunner buildRunner(final List options) => + BetterCommandRunner( + mockCommandName, + mockCommandDescription, + globalOptions: options, + ); + + group('Non-Blank Group Names are rendered as-is', () { + final grouplessOptions = [ + for (var i = 0; i < 5; ++i) buildMockOption(buildMockArgName(i)), + ]; + final groupedOptions = [ + for (var i = 5; i < 10; ++i) + buildMockOption(buildMockArgName(i), buildMockGroup(i)), + ]; + final expectation = allOf([ + for (var i = 5; i < 10; ++i) contains(buildMockGroupName(i)), + ]); test( 'in the presence of Groupless Options', () { expect( - BetterCommandRunner( - mockCommandName, - mockCommandDescription, - globalOptions: [ - for (var i = 0; i < 5; ++i) - buildMockOption(buildMockArgName(i), buildMockGroup(i)), - for (var i = 5; i < 10; ++i) - buildMockOption(buildMockArgName(i), null), - ], - ).usage, - allOf([ - for (var i = 0; i < 5; ++i) - contains(buildSeparatorView(buildMockGroupName(i))), - ]), + buildRunner(grouplessOptions + groupedOptions).usage, + expectation, ); }, ); @@ -74,46 +78,31 @@ void main() { 'in the absence of Groupless Options', () { expect( - BetterCommandRunner( - mockCommandName, - mockCommandDescription, - globalOptions: [ - for (var i = 0; i < 5; ++i) - buildMockOption(buildMockArgName(i), buildMockGroup(i)), - ], - ).usage, - allOf([ - for (var i = 0; i < 5; ++i) - contains(buildSeparatorView(buildMockGroupName(i))), - ]), + buildRunner(groupedOptions).usage, + expectation, ); }, ); }); - group('Blank Group Names are not rendered as-is', () { + group('Group Names are properly padded with newlines', () { + final grouplessOptions = [ + for (var i = 0; i < 5; ++i) buildMockOption(buildMockArgName(i)), + ]; + final groupedOptions = [ + for (var i = 5; i < 10; ++i) + buildMockOption(buildMockArgName(i), buildMockGroup(i)), + ]; + final expectation = allOf([ + for (var i = 5; i < 10; ++i) + contains(buildSeparatorView(buildMockGroupName(i))), + ]); test( 'in the presence of Groupless Options', () { expect( - BetterCommandRunner( - mockCommandName, - mockCommandDescription, - globalOptions: [ - for (var i = 0; i < nBlankNameExamples; ++i) - buildMockOption( - buildMockArgName(i), - OptionGroup(blankNameExamples[i]), - ), - for (var i = nBlankNameExamples; i < nBlankNameExamples + 5; ++i) - buildMockOption(buildMockArgName(i), null), - ], - ).usage, - allOf([ - for (final blankName in blankNameExamples) - if (blankName != '' && blankName != '\n') - isNot(contains(buildSeparatorView(blankName))), - ]), + buildRunner(grouplessOptions + groupedOptions).usage, + expectation, ); }, ); @@ -121,46 +110,36 @@ void main() { 'in the absence of Groupless Options', () { expect( - BetterCommandRunner( - mockCommandName, - mockCommandDescription, - globalOptions: [ - for (var i = 0; i < nBlankNameExamples; ++i) - buildMockOption( - buildMockArgName(i), - OptionGroup(blankNameExamples[i]), - ), - ], - ).usage, - allOf([ - for (final blankName in blankNameExamples) - if (blankName != '' && blankName != '\n') - isNot(contains(buildSeparatorView(blankName))), - ]), + buildRunner(groupedOptions).usage, + expectation, ); }, ); }); - group('Non-Blank Group Names are rendered as-is', () { + group('Blank Group Names are not rendered as-is', () { + final nBlankNameExamples = blankNameExamples.length; + final groupedOptions = [ + for (var i = 0; i < nBlankNameExamples; ++i) + buildMockOption( + buildMockArgName(i), + OptionGroup(blankNameExamples[i]), + ), + ]; + final grouplessOptions = [ + for (var i = nBlankNameExamples; i < nBlankNameExamples + 5; ++i) + buildMockOption(buildMockArgName(i)), + ]; + final expectation = allOf([ + for (final blankName in blankNameExamples) + isNot(contains(buildSeparatorView(blankName))), + ]); test( 'in the presence of Groupless Options', () { expect( - BetterCommandRunner( - mockCommandName, - mockCommandDescription, - globalOptions: [ - for (var i = 0; i < 5; ++i) - buildMockOption(buildMockArgName(i), buildMockGroup(i)), - for (var i = 5; i < 10; ++i) - buildMockOption(buildMockArgName(i), null), - ], - ).usage, - allOf([ - for (var i = 0; i < 5; ++i) - contains(buildSeparatorView(buildMockGroupName(i))), - ]), + buildRunner(grouplessOptions + groupedOptions).usage, + expectation, ); }, ); @@ -168,53 +147,42 @@ void main() { 'in the absence of Groupless Options', () { expect( - BetterCommandRunner( - mockCommandName, - mockCommandDescription, - globalOptions: [ - for (var i = 0; i < 5; ++i) - buildMockOption(buildMockArgName(i), buildMockGroup(i)), - ], - ).usage, - allOf([ - for (var i = 0; i < 5; ++i) - contains(buildSeparatorView(buildMockGroupName(i))), - ]), + buildRunner(groupedOptions).usage, + expectation, ); }, ); }); group('Blank Group Names get a count-based default Group Name', () { + final grouplessOptions = [ + for (var i = 0; i < 5; ++i) buildMockOption(buildMockArgName(i)), + ]; + final groupedOptions = [ + buildMockOption( + buildMockArgName('a'), + OptionGroup(blankNameExamples.first), + ), + buildMockOption( + buildMockArgName('b'), + OptionGroup(buildMockGroupName('XYZ')), + ), + buildMockOption( + buildMockArgName('c'), + OptionGroup(blankNameExamples.last), + ), + ]; + final expectation = stringContainsInOrder([ + buildSeparatorView(buildFallbackGroupName(1)), + buildSeparatorView(buildMockGroupName('XYZ')), + buildSeparatorView(buildFallbackGroupName(3)), + ]); test( 'in the presence of Groupless Options', () { expect( - BetterCommandRunner( - mockCommandName, - mockCommandDescription, - globalOptions: [ - buildMockOption( - buildMockArgName(1), - OptionGroup(blankNameExamples.first), - ), - buildMockOption( - buildMockArgName(2), - OptionGroup(buildMockGroupName('XYZ')), - ), - buildMockOption( - buildMockArgName(3), - OptionGroup(blankNameExamples.last), - ), - for (var i = 100; i < 105; ++i) - buildMockOption(buildMockArgName(i), null), - ], - ).usage, - stringContainsInOrder([ - buildSeparatorView(buildFallbackGroupName(1)), - buildSeparatorView(buildMockGroupName('XYZ')), - buildSeparatorView(buildFallbackGroupName(3)), - ]), + buildRunner(grouplessOptions + groupedOptions).usage, + expectation, ); }, ); @@ -222,29 +190,8 @@ void main() { 'in the absence of Groupless Options', () { expect( - BetterCommandRunner( - mockCommandName, - mockCommandDescription, - globalOptions: [ - buildMockOption( - buildMockArgName(1), - OptionGroup(blankNameExamples.first), - ), - buildMockOption( - buildMockArgName(2), - OptionGroup(buildMockGroupName('XYZ')), - ), - buildMockOption( - buildMockArgName(3), - OptionGroup(blankNameExamples.last), - ), - ], - ).usage, - stringContainsInOrder([ - buildSeparatorView(buildFallbackGroupName(1)), - buildSeparatorView(buildMockGroupName('XYZ')), - buildSeparatorView(buildFallbackGroupName(3)), - ]), + buildRunner(groupedOptions).usage, + expectation, ); }, ); @@ -253,35 +200,29 @@ void main() { test( 'All Groupless Options are shown before Grouped Options', () { - expect( - BetterCommandRunner( - mockCommandName, - mockCommandDescription, - globalOptions: [ - buildMockOption( - buildMockArgName(1), - OptionGroup(blankNameExamples.first), - ), - buildMockOption( - buildMockArgName(2), - OptionGroup(buildMockGroupName('XYZ')), - ), - buildMockOption( - buildMockArgName(3), - OptionGroup(blankNameExamples.last), - ), - for (var i = 100; i < 105; ++i) - buildMockOption(buildMockArgName(i), null), - ], - ).usage, - stringContainsInOrder([ + final groupedOptions = [ + for (var i = 0; i < 5; ++i) + buildMockOption(buildMockArgName(i), buildMockGroup(i)), + ]; + final grouplessOptions = [ + for (var i = 5; i < 10; ++i) buildMockOption(buildMockArgName(i)), + ]; + final expectation = stringContainsInOrder([ + '\n', + for (var i = 5; i < 10; ++i) ...[ + buildMockArgName(i), '\n', - for (var i = 100; i < 105; ++i) buildMockArgName(i), + ], + '\n', + for (var i = 0; i < 5; ++i) ...[ + buildMockArgName(i), '\n', - buildSeparatorView(buildFallbackGroupName(1)), - buildSeparatorView(buildMockGroupName('XYZ')), - buildSeparatorView(buildFallbackGroupName(3)), - ]), + ], + '\n', + ]); + expect( + buildRunner(groupedOptions + grouplessOptions).usage, + expectation, ); }, ); @@ -289,30 +230,37 @@ void main() { test( 'Relative order of all Options within a Group is preserved', () { - var optionCount1 = 0; - var optionCount2 = 0; - expect( - BetterCommandRunner( - mockCommandName, - mockCommandDescription, - globalOptions: [ - for (var i = 0; i < 5; ++i) - buildMockOption(buildMockArgName(++optionCount1), null), - for (var i = 0; i < 3; ++i) - for (var j = 0; j < 5; ++j) - buildMockOption( - buildMockArgName(++optionCount1), - OptionGroup(buildMockGroupName(i)), - ), - ], - ).usage, - stringContainsInOrder([ - '\n', - for (var i = 0; i < 5; ++i) buildMockArgName(++optionCount2), + var testOptionCount = 0; + final grouplessOptions = [ + for (var i = 0; i < 5; ++i) + buildMockOption(buildMockArgName(++testOptionCount)), + ]; + final groupedOptions = [ + for (var i = 0; i < 3; ++i) + for (var j = 0; j < 5; ++j) + buildMockOption( + buildMockArgName(++testOptionCount), + buildMockGroup(i), + ), + ]; + var expectationOptionCount = 0; + final expectation = stringContainsInOrder([ + '\n', + for (var i = 0; i < 5; ++i) ...[ + buildMockArgName(++expectationOptionCount), '\n', - for (var i = 0; i < 3; ++i) - for (var j = 0; j < 5; ++j) buildMockArgName(++optionCount2), - ]), + ], + '\n', + for (var i = 0; i < 3; ++i) + for (var j = 0; j < 5; ++j) ...[ + buildMockArgName(++expectationOptionCount), + '\n', + ], + '\n', + ]); + expect( + buildRunner(grouplessOptions + groupedOptions).usage, + expectation, ); }, ); @@ -321,27 +269,27 @@ void main() { 'Relative order of all Groups is preserved', () { var optionCount = 0; - var groupCount1 = 0; - var groupCount2 = 0; + var testGroupCount = 0; + final grouplessOptions = [ + for (var i = 0; i < 5; ++i) + buildMockOption(buildMockArgName(++optionCount)), + ]; + final groupedOptions = [ + for (var i = 0; i < 3; ++i) + for (var j = 0; j < 5; ++j) + buildMockOption( + buildMockArgName(++optionCount), + buildMockGroup(++testGroupCount), + ), + ]; + var expectationGroupCount = 0; + final expectation = stringContainsInOrder([ + for (var i = 0; i < testGroupCount; ++i) + buildSeparatorView(buildMockGroupName(++expectationGroupCount)), + ]); expect( - BetterCommandRunner( - mockCommandName, - mockCommandDescription, - globalOptions: [ - for (var i = 0; i < 5; ++i) - buildMockOption(buildMockArgName(++optionCount), null), - for (var i = 0; i < 3; ++i) - for (var j = 0; j < 5; ++j) - buildMockOption( - buildMockArgName(++optionCount), - buildMockGroup(++groupCount1), - ), - ], - ).usage, - stringContainsInOrder([ - for (var i = 0; i < groupCount1; ++i) - buildSeparatorView(buildMockGroupName(++groupCount2)), - ]), + buildRunner(grouplessOptions + groupedOptions).usage, + expectation, ); }, ); From ff9368e103d21368226f198e01fc3550a39a2015 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:59:45 +0530 Subject: [PATCH 11/24] test: Ensures no automatic count-based Group Name when only one Group exists --- .../option_group_usage_text_test.dart | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart index b163d25..b522527 100644 --- a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart +++ b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart @@ -197,6 +197,44 @@ void main() { ); }); + group('No automatic count-based Group Name when only one Group exists', () { + final anyBlankGroupName = blankNameExamples.first; + final grouplessOptions = [ + for (var i = 0; i < 5; ++i) buildMockOption(buildMockArgName(i)), + ]; + final groupedOptions = [ + for (var i = 5; i < 10; ++i) + buildMockOption( + buildMockArgName(i), + OptionGroup(anyBlankGroupName), + ), + ]; + final expectation = allOf([ + for (final potentialSuffix in [1, 0, -1]) + isNot( + contains(buildSeparatorView(buildFallbackGroupName(potentialSuffix))), + ), + ]); + test( + 'in the presence of Groupless Options', + () { + expect( + buildRunner(grouplessOptions + groupedOptions).usage, + expectation, + ); + }, + ); + test( + 'in the absence of Groupless Options', + () { + expect( + buildRunner(groupedOptions).usage, + expectation, + ); + }, + ); + }); + test( 'All Groupless Options are shown before Grouped Options', () { From 6e522d8184b34242506f8edc29d54a1aa9a368a5 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Thu, 9 Oct 2025 19:58:15 +0530 Subject: [PATCH 12/24] fix: Consistency with the existing Wrapper around Group Name - entirely removed Fallback Group Name feature - updated corresponding Test Cases with the actual expected behavior --- .../option_group_usage_text_test.dart | 136 +----------------- packages/config/lib/src/config/options.dart | 16 +-- 2 files changed, 2 insertions(+), 150 deletions(-) diff --git a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart index b522527..9ab8021 100644 --- a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart +++ b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart @@ -6,19 +6,6 @@ void main() { const mockCommandName = 'mock'; const mockCommandDescription = 'A mock CLI for Option Group Usage Text test.'; const mockOptionHelpText = 'Help Text for this Mock Option.'; - const defaultFallbackGroupName = 'Option Group'; - const blankNameExamples = [ - '', - ' ', - ' ', - ' ', - '\n', - '\n\n', - '\n\n\n', - '\t', - '\t\t', - '\t\t\t', - ]; String buildSeparatorView(final String name) => '\n\n$name\n'; @@ -31,9 +18,6 @@ void main() { String buildMockGroupName([final Object? suffix = '']) => 'Mock Group${buildSuffix(suffix, ' ')}'; - String buildFallbackGroupName([final Object? suffix = '']) => - '$defaultFallbackGroupName${buildSuffix(suffix, ' ')}'; - OptionGroup buildMockGroup([final Object? suffix = '']) => OptionGroup(buildMockGroupName(suffix)); @@ -54,7 +38,7 @@ void main() { globalOptions: options, ); - group('Non-Blank Group Names are rendered as-is', () { + group('Group Names are rendered as-is', () { final grouplessOptions = [ for (var i = 0; i < 5; ++i) buildMockOption(buildMockArgName(i)), ]; @@ -117,124 +101,6 @@ void main() { ); }); - group('Blank Group Names are not rendered as-is', () { - final nBlankNameExamples = blankNameExamples.length; - final groupedOptions = [ - for (var i = 0; i < nBlankNameExamples; ++i) - buildMockOption( - buildMockArgName(i), - OptionGroup(blankNameExamples[i]), - ), - ]; - final grouplessOptions = [ - for (var i = nBlankNameExamples; i < nBlankNameExamples + 5; ++i) - buildMockOption(buildMockArgName(i)), - ]; - final expectation = allOf([ - for (final blankName in blankNameExamples) - isNot(contains(buildSeparatorView(blankName))), - ]); - test( - 'in the presence of Groupless Options', - () { - expect( - buildRunner(grouplessOptions + groupedOptions).usage, - expectation, - ); - }, - ); - test( - 'in the absence of Groupless Options', - () { - expect( - buildRunner(groupedOptions).usage, - expectation, - ); - }, - ); - }); - - group('Blank Group Names get a count-based default Group Name', () { - final grouplessOptions = [ - for (var i = 0; i < 5; ++i) buildMockOption(buildMockArgName(i)), - ]; - final groupedOptions = [ - buildMockOption( - buildMockArgName('a'), - OptionGroup(blankNameExamples.first), - ), - buildMockOption( - buildMockArgName('b'), - OptionGroup(buildMockGroupName('XYZ')), - ), - buildMockOption( - buildMockArgName('c'), - OptionGroup(blankNameExamples.last), - ), - ]; - final expectation = stringContainsInOrder([ - buildSeparatorView(buildFallbackGroupName(1)), - buildSeparatorView(buildMockGroupName('XYZ')), - buildSeparatorView(buildFallbackGroupName(3)), - ]); - test( - 'in the presence of Groupless Options', - () { - expect( - buildRunner(grouplessOptions + groupedOptions).usage, - expectation, - ); - }, - ); - test( - 'in the absence of Groupless Options', - () { - expect( - buildRunner(groupedOptions).usage, - expectation, - ); - }, - ); - }); - - group('No automatic count-based Group Name when only one Group exists', () { - final anyBlankGroupName = blankNameExamples.first; - final grouplessOptions = [ - for (var i = 0; i < 5; ++i) buildMockOption(buildMockArgName(i)), - ]; - final groupedOptions = [ - for (var i = 5; i < 10; ++i) - buildMockOption( - buildMockArgName(i), - OptionGroup(anyBlankGroupName), - ), - ]; - final expectation = allOf([ - for (final potentialSuffix in [1, 0, -1]) - isNot( - contains(buildSeparatorView(buildFallbackGroupName(potentialSuffix))), - ), - ]); - test( - 'in the presence of Groupless Options', - () { - expect( - buildRunner(grouplessOptions + groupedOptions).usage, - expectation, - ); - }, - ); - test( - 'in the absence of Groupless Options', - () { - expect( - buildRunner(groupedOptions).usage, - expectation, - ); - }, - ); - }); - test( 'All Groupless Options are shown before Grouped Options', () { diff --git a/packages/config/lib/src/config/options.dart b/packages/config/lib/src/config/options.dart index dcf127c..c2ad224 100644 --- a/packages/config/lib/src/config/options.dart +++ b/packages/config/lib/src/config/options.dart @@ -804,7 +804,6 @@ void addOptionsToParser( final Iterable argNameOpts, final ArgParser argParser, { final bool addGroupSeparators = false, - final String defaultFallbackGroupName = 'Option Group', }) { // Plain Option-addition without any Group Logic if (!addGroupSeparators) { @@ -833,26 +832,13 @@ void addOptionsToParser( // Helpers for consistent processing and validation void addOne(final OptionDefinition x) => x.option._addToArgParser(argParser); void addAll(final List options) => options.forEach(addOne); - bool isNotBlank(final String name) => name.trim().isNotEmpty; - String buildFallbackGroupName(final int counter) => - '$defaultFallbackGroupName${optionGroups.length > 1 ? " $counter" : ""}'; // Add all Groupless Options first (in order) addAll(grouplessOptions); // Add all Groups (in order) and their Options (in order) - var groupCounter = 0; optionGroups.forEach((final group, final options) { - ++groupCounter; - final givenGroupName = group.name; - final fallbackGroupName = buildFallbackGroupName(groupCounter); - - // Add the Group Name after handling potential Blank Names - argParser.addSeparator( - isNotBlank(givenGroupName) ? givenGroupName : fallbackGroupName, - ); - - // Add all the Grouped Options (in order) + argParser.addSeparator(group.name); addAll(options); }); } From 51abfbd98210c2d0a88ba3d1a90f97b1b4f2a5aa Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Thu, 9 Oct 2025 20:06:24 +0530 Subject: [PATCH 13/24] docs: Improvement to minor inline documentation Explicit mention of Ordered Containers. --- packages/config/lib/src/config/options.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/config/lib/src/config/options.dart b/packages/config/lib/src/config/options.dart index c2ad224..5a27aef 100644 --- a/packages/config/lib/src/config/options.dart +++ b/packages/config/lib/src/config/options.dart @@ -813,9 +813,11 @@ void addOptionsToParser( return; } + // The following containers are ordered by default + final optionGroups = >{}; + final grouplessOptions = []; + // Gather all necessary Option Group information - final optionGroups = >{}; // ordered map - final grouplessOptions = []; // ordered list for (final opt in argNameOpts) { final group = opt.option.group; if (group != null) { From 173d6deee7b9e9598fc8c4aa24687b1e4a4f4a7d Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Thu, 9 Oct 2025 21:12:11 +0530 Subject: [PATCH 14/24] test: Unit Test for jumbled Option Groups Ensures that the relative order of all Groups and Options is preserved. --- .../option_group_usage_text_test.dart | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart index 9ab8021..036f34a 100644 --- a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart +++ b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart @@ -197,4 +197,54 @@ void main() { ); }, ); + + test( + 'Relative order of all Groups and Options is preserved', + () { + expect( + buildRunner(const [ + FlagOption( + argName: 'option-1', + group: null, + helpText: 'Help section for option-1.', + ), + FlagOption( + argName: 'option-2', + group: OptionGroup('Group 1'), + helpText: 'Help section for option-2.', + ), + FlagOption( + argName: 'option-3', + group: OptionGroup('Group 2'), + helpText: 'Help section for option-3.', + ), + FlagOption( + argName: 'option-4', + group: OptionGroup('Group 1'), + helpText: 'Help section for option-4.', + ), + FlagOption( + argName: 'option-5', + group: null, + helpText: 'Help section for option-5.', + ), + FlagOption( + argName: 'option-6', + group: OptionGroup('Group 2'), + helpText: 'Help section for option-6.', + ), + ]).usage, + stringContainsInOrder([ + 'option-1', + 'option-5', + 'Group 1', + 'option-2', + 'option-4', + 'Group 2', + 'option-3', + 'option-6', + ]), + ); + }, + ); } From 293ac9e664b33c72a2f05851e186522a4a53e5d8 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Fri, 10 Oct 2025 00:14:49 +0530 Subject: [PATCH 15/24] docs(OptionGroup): Public Comment updated Reflects the correct Option Group Name behavior. --- packages/config/lib/src/config/options.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/config/lib/src/config/options.dart b/packages/config/lib/src/config/options.dart index 5a27aef..e05bc1b 100644 --- a/packages/config/lib/src/config/options.dart +++ b/packages/config/lib/src/config/options.dart @@ -41,7 +41,7 @@ abstract interface class OptionDefinition { /// An option group allows grouping options together under a common name, /// and optionally provide option value validation on the group as a whole. /// -/// [name] might be used as group header in usage information +/// [name] shall be used as group header in usage information /// so it is recommended to format it appropriately, e.g. `File mode`. /// /// An [OptionGroup] is uniquely identified by its [name]. From 67ec08dac8b35cfa8d05de0be1609b3a0531bf13 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Fri, 10 Oct 2025 00:29:20 +0530 Subject: [PATCH 16/24] refactor: Consistent processing Also addresses a nitpick comment by the Review Bot. --- packages/config/lib/src/config/options.dart | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/config/lib/src/config/options.dart b/packages/config/lib/src/config/options.dart index e05bc1b..740ab12 100644 --- a/packages/config/lib/src/config/options.dart +++ b/packages/config/lib/src/config/options.dart @@ -805,11 +805,13 @@ void addOptionsToParser( final ArgParser argParser, { final bool addGroupSeparators = false, }) { + // Helpers for consistent processing + void addOne(final OptionDefinition o) => o.option._addToArgParser(argParser); + void addAll(final Iterable o) => o.forEach(addOne); + // Plain Option-addition without any Group Logic if (!addGroupSeparators) { - for (final opt in argNameOpts) { - opt.option._addToArgParser(argParser); - } + addAll(argNameOpts); return; } @@ -831,10 +833,6 @@ void addOptionsToParser( } } - // Helpers for consistent processing and validation - void addOne(final OptionDefinition x) => x.option._addToArgParser(argParser); - void addAll(final List options) => options.forEach(addOne); - // Add all Groupless Options first (in order) addAll(grouplessOptions); From f08dc50be3de9c6fc47d2c0746ef66ae7614a426 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Fri, 10 Oct 2025 12:22:28 +0530 Subject: [PATCH 17/24] refactor: Improved readability of `addOptionsToParser` - inlined small helper functions - comments emphasize the intent behind modified implemention - added external reference for explanation of Ordered Map in Dart --- packages/config/lib/src/config/options.dart | 29 ++++++++++++--------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/config/lib/src/config/options.dart b/packages/config/lib/src/config/options.dart index 740ab12..f27ffbb 100644 --- a/packages/config/lib/src/config/options.dart +++ b/packages/config/lib/src/config/options.dart @@ -805,17 +805,18 @@ void addOptionsToParser( final ArgParser argParser, { final bool addGroupSeparators = false, }) { - // Helpers for consistent processing - void addOne(final OptionDefinition o) => o.option._addToArgParser(argParser); - void addAll(final Iterable o) => o.forEach(addOne); - - // Plain Option-addition without any Group Logic + // Plain Option-addition without any Group Separator Logic if (!addGroupSeparators) { - addAll(argNameOpts); + for (final o in argNameOpts) { + o.option._addToArgParser(argParser); + } return; } - // The following containers are ordered by default + // The following containers are ordered by default i.e. + // preserves insertion-order: + // - Map : https://stackoverflow.com/q/79786585/10251345 + // - List : https://api.dart.dev/dart-core/List-class.html final optionGroups = >{}; final grouplessOptions = []; @@ -833,13 +834,17 @@ void addOptionsToParser( } } - // Add all Groupless Options first (in order) - addAll(grouplessOptions); + // Add all Groupless Options FIRST (in order) + for (final o in grouplessOptions) { + o.option._addToArgParser(argParser); + } - // Add all Groups (in order) and their Options (in order) - optionGroups.forEach((final group, final options) { + // Add all EXPLICIT Groups (in order) and their Options (in order) + optionGroups.forEach((final group, final groupedOptions) { argParser.addSeparator(group.name); - addAll(options); + for (final o in groupedOptions) { + o.option._addToArgParser(argParser); + } }); } From 4cda148c42e72c9da0cf207ab3ecfcfa42a78ce9 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Fri, 10 Oct 2025 12:42:00 +0530 Subject: [PATCH 18/24] fix: Empty Headers avoided - for Groups with only Hidden Options - minor Comment improvements --- packages/config/lib/src/config/options.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/config/lib/src/config/options.dart b/packages/config/lib/src/config/options.dart index f27ffbb..b8330f0 100644 --- a/packages/config/lib/src/config/options.dart +++ b/packages/config/lib/src/config/options.dart @@ -805,7 +805,7 @@ void addOptionsToParser( final ArgParser argParser, { final bool addGroupSeparators = false, }) { - // Plain Option-addition without any Group Separator Logic + // Plain Option-addition WITHOUT any Group Separator Logic if (!addGroupSeparators) { for (final o in argNameOpts) { o.option._addToArgParser(argParser); @@ -839,9 +839,13 @@ void addOptionsToParser( o.option._addToArgParser(argParser); } - // Add all EXPLICIT Groups (in order) and their Options (in order) + // Add all EXPLICIT Groups (in order) optionGroups.forEach((final group, final groupedOptions) { - argParser.addSeparator(group.name); + // Add the Group Name Separator only if it has at least one Visible Option + if (groupedOptions.any((final o) => !o.option.hide)) { + argParser.addSeparator(group.name); + } + // Add all Options WITHIN this Group (in order) for (final o in groupedOptions) { o.option._addToArgParser(argParser); } From 78dc901a34f33c9ec6ab8b784d93de9a1e1b5992 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Fri, 10 Oct 2025 13:05:14 +0530 Subject: [PATCH 19/24] test: Hidden Groups are avoided as Group Name Separator - Test Cases for the recent fix (bug caught by the Review Bot) - minor Comment improvement --- .../option_group_usage_text_test.dart | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart index 036f34a..bcd5df9 100644 --- a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart +++ b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart @@ -31,6 +31,17 @@ void main() { helpText: mockOptionHelpText, ); + OptionDefinition buildHiddenMockOption( + final String name, [ + final OptionGroup? group, + ]) => + FlagOption( + argName: name, + group: group, + hide: true, + helpText: mockOptionHelpText, + ); + BetterCommandRunner buildRunner(final List options) => BetterCommandRunner( mockCommandName, @@ -38,7 +49,7 @@ void main() { globalOptions: options, ); - group('Group Names are rendered as-is', () { + group('Group Names (of visible Groups) are rendered as-is', () { final grouplessOptions = [ for (var i = 0; i < 5; ++i) buildMockOption(buildMockArgName(i)), ]; @@ -69,6 +80,54 @@ void main() { ); }); + group('Group Names (of invisible Groups) are hidden', () { + var testOptionCount = 0; + var testGroupCount = 0; + final grouplessOptions = [ + for (var i = 0; i < 5; ++i) + buildMockOption(buildMockArgName(++testOptionCount)), + ]; + final groupedOptions = [ + for (var i = 0; i < 5; ++i) + buildMockOption( + buildMockArgName(++testOptionCount), + buildMockGroup(++testGroupCount), + ), + ]; + final hiddenGroups = [ + for (var i = 0; i < 5; ++i) + buildHiddenMockOption( + buildMockArgName(++testOptionCount), + buildMockGroup(++testGroupCount), + ), + ]; + var expectationGroupCount = 0; + final expectation = allOf([ + for (var i = 0; i < 5; ++i) + contains(buildMockGroupName(++expectationGroupCount)), + for (var i = 0; i < 5; ++i) + isNot(contains(buildMockGroupName(++expectationGroupCount))), + ]); + test( + 'in the presence of Groupless Options', + () { + expect( + buildRunner(grouplessOptions + groupedOptions + hiddenGroups).usage, + expectation, + ); + }, + ); + test( + 'in the absence of Groupless Options', + () { + expect( + buildRunner(groupedOptions + hiddenGroups).usage, + expectation, + ); + }, + ); + }); + group('Group Names are properly padded with newlines', () { final grouplessOptions = [ for (var i = 0; i < 5; ++i) buildMockOption(buildMockArgName(i)), @@ -199,7 +258,7 @@ void main() { ); test( - 'Relative order of all Groups and Options is preserved', + 'Relative order of jumbled Groups and Options is preserved', () { expect( buildRunner(const [ From 98cd918cf7913a315c4a5a7104121fedc9317d10 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Fri, 10 Oct 2025 13:17:02 +0530 Subject: [PATCH 20/24] test: Combined Behavior check made more rigorous Combination of: - Groupless Options - Grouped Options - Hidden Groups --- .../option_group_usage_text_test.dart | 60 +++++++++++++++---- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart index bcd5df9..36a51b4 100644 --- a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart +++ b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart @@ -258,7 +258,7 @@ void main() { ); test( - 'Relative order of jumbled Groups and Options is preserved', + 'Combined Behavior check (Groupless Options, Grouped Options, Hidden Groups)', () { expect( buildRunner(const [ @@ -292,16 +292,56 @@ void main() { group: OptionGroup('Group 2'), helpText: 'Help section for option-6.', ), + FlagOption( + argName: 'option-7', + group: OptionGroup('Group 3'), + hide: true, + helpText: 'Help section for option-7.', + ), + FlagOption( + argName: 'option-8', + group: OptionGroup('Group 4'), + hide: true, + helpText: 'Help section for option-8.', + ), + FlagOption( + argName: 'option-9', + group: OptionGroup('Group 4'), + hide: true, + helpText: 'Help section for option-9.', + ), + FlagOption( + argName: 'option-10', + group: OptionGroup('Group 5'), + hide: true, + helpText: 'Help section for option-10.', + ), + FlagOption( + argName: 'option-11', + group: OptionGroup('Group 5'), + hide: false, + helpText: 'Help section for option-11.', + ), ]).usage, - stringContainsInOrder([ - 'option-1', - 'option-5', - 'Group 1', - 'option-2', - 'option-4', - 'Group 2', - 'option-3', - 'option-6', + allOf([ + stringContainsInOrder([ + 'option-1', + 'option-5', + 'Group 1', + 'option-2', + 'option-4', + 'Group 2', + 'option-3', + 'option-6', + 'Group 5', + 'option-11', + ]), + isNot(contains('Group 3')), + isNot(contains('option-7')), + isNot(contains('Group 4')), + isNot(contains('option-8')), + isNot(contains('option-9')), + isNot(contains('option-10')), ]), ); }, From 2f497192cfc3dd413711fe87caba011041d85137 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:09:04 +0530 Subject: [PATCH 21/24] refactor: Readability of Test Cases (for Group Name) improved - more compact Helper Functions - better Call Site readability --- .../option_group_usage_text_test.dart | 131 +++++------------- 1 file changed, 35 insertions(+), 96 deletions(-) diff --git a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart index 36a51b4..7433216 100644 --- a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart +++ b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart @@ -5,7 +5,6 @@ import 'package:test/test.dart'; void main() { const mockCommandName = 'mock'; const mockCommandDescription = 'A mock CLI for Option Group Usage Text test.'; - const mockOptionHelpText = 'Help Text for this Mock Option.'; String buildSeparatorView(final String name) => '\n\n$name\n'; @@ -18,28 +17,16 @@ void main() { String buildMockGroupName([final Object? suffix = '']) => 'Mock Group${buildSuffix(suffix, ' ')}'; - OptionGroup buildMockGroup([final Object? suffix = '']) => - OptionGroup(buildMockGroupName(suffix)); - OptionDefinition buildMockOption( - final String name, [ - final OptionGroup? group, - ]) => + final String argName, + final String? groupName, { + final bool hide = false, + }) => FlagOption( - argName: name, - group: group, - helpText: mockOptionHelpText, - ); - - OptionDefinition buildHiddenMockOption( - final String name, [ - final OptionGroup? group, - ]) => - FlagOption( - argName: name, - group: group, - hide: true, - helpText: mockOptionHelpText, + argName: argName, + hide: hide, + group: groupName != null ? OptionGroup(groupName) : null, + helpText: 'Help section for $argName.', ); BetterCommandRunner buildRunner(final List options) => @@ -51,11 +38,11 @@ void main() { group('Group Names (of visible Groups) are rendered as-is', () { final grouplessOptions = [ - for (var i = 0; i < 5; ++i) buildMockOption(buildMockArgName(i)), + for (var i = 0; i < 5; ++i) buildMockOption(buildMockArgName(i), null), ]; final groupedOptions = [ for (var i = 5; i < 10; ++i) - buildMockOption(buildMockArgName(i), buildMockGroup(i)), + buildMockOption(buildMockArgName(i), buildMockGroupName(i)), ]; final expectation = allOf([ for (var i = 5; i < 10; ++i) contains(buildMockGroupName(i)), @@ -85,20 +72,21 @@ void main() { var testGroupCount = 0; final grouplessOptions = [ for (var i = 0; i < 5; ++i) - buildMockOption(buildMockArgName(++testOptionCount)), + buildMockOption(buildMockArgName(++testOptionCount), null), ]; final groupedOptions = [ for (var i = 0; i < 5; ++i) buildMockOption( buildMockArgName(++testOptionCount), - buildMockGroup(++testGroupCount), + buildMockGroupName(++testGroupCount), ), ]; final hiddenGroups = [ for (var i = 0; i < 5; ++i) - buildHiddenMockOption( + buildMockOption( buildMockArgName(++testOptionCount), - buildMockGroup(++testGroupCount), + buildMockGroupName(++testGroupCount), + hide: true, ), ]; var expectationGroupCount = 0; @@ -130,11 +118,11 @@ void main() { group('Group Names are properly padded with newlines', () { final grouplessOptions = [ - for (var i = 0; i < 5; ++i) buildMockOption(buildMockArgName(i)), + for (var i = 0; i < 5; ++i) buildMockOption(buildMockArgName(i), null), ]; final groupedOptions = [ for (var i = 5; i < 10; ++i) - buildMockOption(buildMockArgName(i), buildMockGroup(i)), + buildMockOption(buildMockArgName(i), buildMockGroupName(i)), ]; final expectation = allOf([ for (var i = 5; i < 10; ++i) @@ -165,10 +153,10 @@ void main() { () { final groupedOptions = [ for (var i = 0; i < 5; ++i) - buildMockOption(buildMockArgName(i), buildMockGroup(i)), + buildMockOption(buildMockArgName(i), buildMockGroupName(i)), ]; final grouplessOptions = [ - for (var i = 5; i < 10; ++i) buildMockOption(buildMockArgName(i)), + for (var i = 5; i < 10; ++i) buildMockOption(buildMockArgName(i), null), ]; final expectation = stringContainsInOrder([ '\n', @@ -196,14 +184,14 @@ void main() { var testOptionCount = 0; final grouplessOptions = [ for (var i = 0; i < 5; ++i) - buildMockOption(buildMockArgName(++testOptionCount)), + buildMockOption(buildMockArgName(++testOptionCount), null), ]; final groupedOptions = [ for (var i = 0; i < 3; ++i) for (var j = 0; j < 5; ++j) buildMockOption( buildMockArgName(++testOptionCount), - buildMockGroup(i), + buildMockGroupName(i), ), ]; var expectationOptionCount = 0; @@ -235,14 +223,14 @@ void main() { var testGroupCount = 0; final grouplessOptions = [ for (var i = 0; i < 5; ++i) - buildMockOption(buildMockArgName(++optionCount)), + buildMockOption(buildMockArgName(++optionCount), null), ]; final groupedOptions = [ for (var i = 0; i < 3; ++i) for (var j = 0; j < 5; ++j) buildMockOption( buildMockArgName(++optionCount), - buildMockGroup(++testGroupCount), + buildMockGroupName(++testGroupCount), ), ]; var expectationGroupCount = 0; @@ -261,67 +249,18 @@ void main() { 'Combined Behavior check (Groupless Options, Grouped Options, Hidden Groups)', () { expect( - buildRunner(const [ - FlagOption( - argName: 'option-1', - group: null, - helpText: 'Help section for option-1.', - ), - FlagOption( - argName: 'option-2', - group: OptionGroup('Group 1'), - helpText: 'Help section for option-2.', - ), - FlagOption( - argName: 'option-3', - group: OptionGroup('Group 2'), - helpText: 'Help section for option-3.', - ), - FlagOption( - argName: 'option-4', - group: OptionGroup('Group 1'), - helpText: 'Help section for option-4.', - ), - FlagOption( - argName: 'option-5', - group: null, - helpText: 'Help section for option-5.', - ), - FlagOption( - argName: 'option-6', - group: OptionGroup('Group 2'), - helpText: 'Help section for option-6.', - ), - FlagOption( - argName: 'option-7', - group: OptionGroup('Group 3'), - hide: true, - helpText: 'Help section for option-7.', - ), - FlagOption( - argName: 'option-8', - group: OptionGroup('Group 4'), - hide: true, - helpText: 'Help section for option-8.', - ), - FlagOption( - argName: 'option-9', - group: OptionGroup('Group 4'), - hide: true, - helpText: 'Help section for option-9.', - ), - FlagOption( - argName: 'option-10', - group: OptionGroup('Group 5'), - hide: true, - helpText: 'Help section for option-10.', - ), - FlagOption( - argName: 'option-11', - group: OptionGroup('Group 5'), - hide: false, - helpText: 'Help section for option-11.', - ), + buildRunner([ + buildMockOption('option-1', null), + buildMockOption('option-2', 'Group 1'), + buildMockOption('option-3', 'Group 2'), + buildMockOption('option-4', 'Group 1'), + buildMockOption('option-5', null), + buildMockOption('option-6', 'Group 2'), + buildMockOption('option-7', 'Group 3', hide: true), + buildMockOption('option-8', 'Group 4', hide: true), + buildMockOption('option-9', 'Group 4', hide: true), + buildMockOption('option-10', 'Group 5', hide: true), + buildMockOption('option-11', 'Group 5'), ]).usage, allOf([ stringContainsInOrder([ From 2a4bbaecb7e568096732be87e4e1b0e5e4c19361 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:27:31 +0530 Subject: [PATCH 22/24] style: usage of CAPS and Caps Adjusted according to the guidelines of a Package Admin. --- packages/config/lib/src/config/options.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/config/lib/src/config/options.dart b/packages/config/lib/src/config/options.dart index b8330f0..b8c2bda 100644 --- a/packages/config/lib/src/config/options.dart +++ b/packages/config/lib/src/config/options.dart @@ -805,7 +805,7 @@ void addOptionsToParser( final ArgParser argParser, { final bool addGroupSeparators = false, }) { - // Plain Option-addition WITHOUT any Group Separator Logic + // plain option-addition without any group separator logic if (!addGroupSeparators) { for (final o in argNameOpts) { o.option._addToArgParser(argParser); @@ -813,14 +813,14 @@ void addOptionsToParser( return; } - // The following containers are ordered by default i.e. + // the following containers are ordered by default i.e. // preserves insertion-order: // - Map : https://stackoverflow.com/q/79786585/10251345 // - List : https://api.dart.dev/dart-core/List-class.html final optionGroups = >{}; final grouplessOptions = []; - // Gather all necessary Option Group information + // gather all necessary option-group information for (final opt in argNameOpts) { final group = opt.option.group; if (group != null) { @@ -834,18 +834,18 @@ void addOptionsToParser( } } - // Add all Groupless Options FIRST (in order) + // add all groupless-options first (in order) for (final o in grouplessOptions) { o.option._addToArgParser(argParser); } - // Add all EXPLICIT Groups (in order) + // add all explicit groups (in order) optionGroups.forEach((final group, final groupedOptions) { - // Add the Group Name Separator only if it has at least one Visible Option + // add the group-name-separator only if it has at least one visible option if (groupedOptions.any((final o) => !o.option.hide)) { argParser.addSeparator(group.name); } - // Add all Options WITHIN this Group (in order) + // add all options within this group (in order) for (final o in groupedOptions) { o.option._addToArgParser(argParser); } From 24c1b28c254e3217ea0cdc2410ea6ba2644c33ff Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:01:58 +0530 Subject: [PATCH 23/24] test: Group Name Separators uniqueness Ensures that only one Separator is added per unique Group Name. --- .../option_group_usage_text_test.dart | 80 ++++++++++++++++--- 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart index 7433216..0117c2f 100644 --- a/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart +++ b/packages/cli_tools/test/better_command_runner/option_group_usage_text_test.dart @@ -36,6 +36,9 @@ void main() { globalOptions: options, ); + int howManyMatches(final String pattern, final String target) => + RegExp(pattern).allMatches(target).length; + group('Group Names (of visible Groups) are rendered as-is', () { final grouplessOptions = [ for (var i = 0; i < 5; ++i) buildMockOption(buildMockArgName(i), null), @@ -148,6 +151,53 @@ void main() { ); }); + group('Only one Separator per unique Group Name', () { + final grouplessOptions = [ + for (var i = 0; i < 5; ++i) buildMockOption(buildMockArgName(i), null), + ]; + final groupedOptions = [ + for (var i = 5; i < 10; ++i) + buildMockOption(buildMockArgName(i), buildMockGroupName('A')), + for (var i = 10; i < 15; ++i) + buildMockOption(buildMockArgName(i), buildMockGroupName('B')), + for (var i = 15; i < 20; ++i) + buildMockOption(buildMockArgName(i), buildMockGroupName('A')), + ]; + void checkExpectation(final String usage) { + expect( + usage, + stringContainsInOrder([ + buildSeparatorView(buildMockGroupName('A')), + for (var i = 5; i < 10; ++i) buildMockArgName(i), + for (var i = 15; i < 20; ++i) buildMockArgName(i), + buildSeparatorView(buildMockGroupName('B')), + for (var i = 10; i < 15; ++i) buildMockArgName(i), + ]), + ); + expect( + howManyMatches(buildSeparatorView(buildMockGroupName('A')), usage), + equals(1), + ); + expect( + howManyMatches(buildSeparatorView(buildMockGroupName('B')), usage), + equals(1), + ); + } + + test( + 'in the presence of Groupless Options', + () { + checkExpectation(buildRunner(grouplessOptions + groupedOptions).usage); + }, + ); + test( + 'in the absence of Groupless Options', + () { + checkExpectation(buildRunner(groupedOptions).usage); + }, + ); + }); + test( 'All Groupless Options are shown before Grouped Options', () { @@ -248,20 +298,21 @@ void main() { test( 'Combined Behavior check (Groupless Options, Grouped Options, Hidden Groups)', () { + final usage = buildRunner([ + buildMockOption('option-1', null), + buildMockOption('option-2', 'Group 1'), + buildMockOption('option-3', 'Group 2'), + buildMockOption('option-4', 'Group 1'), + buildMockOption('option-5', null), + buildMockOption('option-6', 'Group 2'), + buildMockOption('option-7', 'Group 3', hide: true), + buildMockOption('option-8', 'Group 4', hide: true), + buildMockOption('option-9', 'Group 4', hide: true), + buildMockOption('option-10', 'Group 5', hide: true), + buildMockOption('option-11', 'Group 5'), + ]).usage; expect( - buildRunner([ - buildMockOption('option-1', null), - buildMockOption('option-2', 'Group 1'), - buildMockOption('option-3', 'Group 2'), - buildMockOption('option-4', 'Group 1'), - buildMockOption('option-5', null), - buildMockOption('option-6', 'Group 2'), - buildMockOption('option-7', 'Group 3', hide: true), - buildMockOption('option-8', 'Group 4', hide: true), - buildMockOption('option-9', 'Group 4', hide: true), - buildMockOption('option-10', 'Group 5', hide: true), - buildMockOption('option-11', 'Group 5'), - ]).usage, + usage, allOf([ stringContainsInOrder([ 'option-1', @@ -283,6 +334,9 @@ void main() { isNot(contains('option-10')), ]), ); + expect(howManyMatches('Group 1', usage), equals(1)); + expect(howManyMatches('Group 2', usage), equals(1)); + expect(howManyMatches('Group 5', usage), equals(1)); }, ); } From 0c947b26e97d3c898cbae4eafec546b7bace61b1 Mon Sep 17 00:00:00 2001 From: Indraneel Rajeevan <105813454+indraneel12@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:16:26 +0530 Subject: [PATCH 24/24] docs: Public Documentation added to cover the new behavior - explained the new feature - explained the reason behind a default value --- packages/config/lib/src/config/options.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/config/lib/src/config/options.dart b/packages/config/lib/src/config/options.dart index b8c2bda..83b7e6b 100644 --- a/packages/config/lib/src/config/options.dart +++ b/packages/config/lib/src/config/options.dart @@ -800,6 +800,19 @@ Iterable validateOptions( return argNameOpts.values; } +/// Adds [argNameOpts] to [argParser]. +/// +/// When [addGroupSeparators] is `true`, +/// - options are grouped by [OptionGroup] and a +/// - separator with the group name is inserted +/// - before each group that has at least one visible option. +/// - Note: +/// - groupless options are added first in their original order +/// - relative order of all options within a group is preserved +/// - relative order of all groups is preserved +/// +/// By default, +/// [addGroupSeparators] is `false` to ensure backwards compatibility. void addOptionsToParser( final Iterable argNameOpts, final ArgParser argParser, {