Skip to content

Commit ea3f5db

Browse files
authored
feat: suggest sub commands (#22)
1 parent ca273fc commit ea3f5db

19 files changed

+927
-320
lines changed

example/lib/src/command_runner.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@ class ExampleCommandRunner extends CompletionCommandRunner<int> {
2121
}) : _logger = logger ?? Logger(),
2222
super(executableName, description) {
2323
// Add root options and flags
24-
argParser.addFlag(
25-
'rootFlag',
26-
help: 'A flag in the root command',
27-
);
24+
argParser
25+
..addFlag(
26+
'rootFlag',
27+
abbr: 'f',
28+
help: 'A flag: in the root command',
29+
)
30+
..addOption('rootOption');
2831

2932
// Add sub commands
3033
addCommand(SomeCommand(_logger));

example/lib/src/commands/some_commmand.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class SomeCommand extends Command<int> {
2323
},
2424
mandatory: true,
2525
)
26+
..addSeparator('yay')
2627
..addOption(
2728
'hidden',
2829
hide: true,

example/test/integrated/completion_integrated_test.dart renamed to example/test/integration/completion_integration_test.dart

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,25 @@ void main() {
1717
'some_other_command': 'This is help for some_other_command',
1818
'--help': 'Print this usage information.',
1919
'--rootFlag': r'A flag\: in the root command',
20+
'--rootOption': null,
2021
};
2122

2223
testCompletion(
2324
'basic usage',
2425
forLine: 'example_cli',
2526
suggests: allRootOptionsAndSubcommands,
26-
skip: notImplemmentedYet,
2727
);
2828

2929
testCompletion(
3030
'leading whitespaces',
3131
forLine: ' example_cli',
3232
suggests: allRootOptionsAndSubcommands,
33-
skip: notImplemmentedYet,
3433
);
3534

3635
testCompletion(
3736
'trailing whitespaces',
3837
forLine: 'example_cli ',
3938
suggests: allRootOptionsAndSubcommands,
40-
skip: notImplemmentedYet,
4139
);
4240
});
4341

@@ -49,6 +47,7 @@ void main() {
4947
suggests: {
5048
'--help': 'Print this usage information.',
5149
'--rootFlag': r'A flag\: in the root command',
50+
'--rootOption': null,
5251
},
5352
skip: notImplemmentedYet,
5453
);
@@ -58,6 +57,7 @@ void main() {
5857
forLine: 'example_cli --r',
5958
suggests: {
6059
'--rootFlag': r'A flag\: in the root command',
60+
'--rootOption': null,
6161
},
6262
skip: notImplemmentedYet,
6363
);
@@ -67,6 +67,7 @@ void main() {
6767
forLine: 'example_cli -h --r',
6868
suggests: {
6969
'--rootFlag': r'A flag\: in the root command',
70+
'--rootOption': null,
7071
},
7172
skip: notImplemmentedYet,
7273
);
@@ -108,7 +109,6 @@ void main() {
108109
'some_command': 'This is help for some_command',
109110
'some_other_command': 'This is help for some_other_command',
110111
},
111-
skip: notImplemmentedYet,
112112
);
113113

114114
testCompletion(
@@ -118,7 +118,6 @@ void main() {
118118
'some_command': 'This is help for some_command',
119119
'some_other_command': 'This is help for some_other_command',
120120
},
121-
skip: notImplemmentedYet,
122121
);
123122

124123
testCompletion(
@@ -127,7 +126,6 @@ void main() {
127126
suggests: {
128127
'some_command': 'This is help for some_command',
129128
},
130-
skip: notImplemmentedYet,
131129
);
132130
});
133131

@@ -138,7 +136,6 @@ void main() {
138136
suggests: {
139137
'melon': 'This is help for some_command',
140138
},
141-
skip: notImplemmentedYet,
142139
);
143140

144141
testCompletion(
@@ -147,7 +144,6 @@ void main() {
147144
suggests: {
148145
r'disguised\:some_commmand': 'This is help for some_command',
149146
},
150-
skip: notImplemmentedYet,
151147
);
152148
});
153149

@@ -172,55 +168,50 @@ void main() {
172168
'--continuous': r'A continuous option\: any value is allowed',
173169
'--multi-d': 'An discrete option that can be passed multiple times ',
174170
'--multi-c': 'An continuous option that can be passed multiple times',
175-
'--flag': '',
171+
'--flag': null,
176172
'--inverseflag': 'A flag that the default value is true',
177173
'--trueflag': 'A flag that cannot be negated'
178174
};
179175

180-
final allAbbreviationssInThisLevel = <String, String>{
176+
final allAbbreviationssInThisLevel = <String, String?>{
181177
'-h': 'Print this usage information.',
182178
'-d': 'A discrete option with "allowed" values (mandatory)',
183179
'-m': 'An discrete option that can be passed multiple times ',
184180
'-n': 'An continuous option that can be passed multiple times',
185-
'-f': '',
181+
'-f': null,
186182
'-i': 'A flag that the default value is true',
187183
'-t': 'A flag that cannot be negated'
188184
};
189185

190-
group('empty', () {
186+
group('empty ', () {
191187
testCompletion(
192188
'basic usage',
193189
forLine: 'example_cli some_command',
194190
suggests: allOptionsInThisLevel,
195-
skip: notImplemmentedYet,
196191
);
197192

198193
testCompletion(
199194
'leading spaces',
200195
forLine: ' example_cli some_command',
201196
suggests: allOptionsInThisLevel,
202-
skip: notImplemmentedYet,
203197
);
204198

205199
testCompletion(
206200
'trailing spaces',
207201
forLine: 'example_cli some_command ',
208202
suggests: allOptionsInThisLevel,
209-
skip: notImplemmentedYet,
210203
);
211204

212205
testCompletion(
213206
'options in between',
214-
forLine: 'example_cli -f some_command',
207+
forLine: 'example_cli -f --rootOption yay some_command',
215208
suggests: allOptionsInThisLevel,
216-
skip: notImplemmentedYet,
217209
);
218210

219211
testCompletion(
220212
'lots of spaces in between',
221213
forLine: 'example_cli some_command',
222214
suggests: allOptionsInThisLevel,
223-
skip: notImplemmentedYet,
224215
);
225216
});
226217

@@ -229,14 +220,12 @@ void main() {
229220
'shows same options for alias sub command',
230221
forLine: 'example_cli melon',
231222
suggests: allOptionsInThisLevel,
232-
skip: notImplemmentedYet,
233223
);
234224

235225
testCompletion(
236226
'shows same options for alias sub command 2',
237227
forLine: 'example_cli disguised:some_commmand',
238228
suggests: allOptionsInThisLevel,
239-
skip: notImplemmentedYet,
240229
);
241230
});
242231

@@ -470,19 +459,19 @@ void main() {
470459
'subcommand': 'A sub command of some_other_command',
471460
'--help': 'Print this usage information.',
472461
},
473-
skip: notImplemmentedYet,
474462
);
475463
});
464+
476465
group('partially written sub command', () {
477466
testCompletion(
478467
'partially written sub command',
479468
forLine: 'example_cli some_other_command sub',
480469
suggests: {
481470
'subcommand': 'A sub command of some_other_command',
482471
},
483-
skip: notImplemmentedYet,
484472
);
485473
});
474+
486475
group('subcommand', () {
487476
final allOptionsInThisLevel = <String, String?>{
488477
'--help': 'Print this usage information.',
@@ -516,7 +505,6 @@ void main() {
516505
'basic usage with args in between',
517506
forLine: 'example_cli some_other_command subcommand_alias',
518507
suggests: allOptionsInThisLevel,
519-
skip: notImplemmentedYet,
520508
);
521509
});
522510
});

example/test/src/command_runner_test.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ Usage: example_cli <command> [arguments]
8181
8282
Global options:
8383
-h, --help Print this usage information.
84-
--[no-]rootFlag A flag in the root command
84+
-f, --[no-]rootFlag A flag: in the root command
85+
--rootOption
8586
8687
Available commands:
8788
some_command This is help for some_command

lib/src/command_runner/commands/handle_completion_command.dart

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,21 @@ import 'dart:async';
22

33
import 'package:args/command_runner.dart';
44
import 'package:cli_completion/src/command_runner/completion_command_runner.dart';
5+
import 'package:cli_completion/src/handling/completion_level.dart';
56
import 'package:cli_completion/src/handling/completion_state.dart';
67
import 'package:cli_completion/src/handling/parser.dart';
7-
import 'package:mason_logger/mason_logger.dart';
88

99
/// {@template handle_completion_request_command}
1010
/// A hidden [Command] added by [CompletionCommandRunner] that handles the
1111
/// "completion" sub command.
12+
/// {@endtemplate}
13+
///
1214
/// This is called by a shell function when the user presses "tab".
1315
/// Any output to stdout during this call will be interpreted as suggestions
1416
/// for completions.
15-
/// {@endtemplate}
1617
class HandleCompletionRequestCommand<T> extends Command<T> {
1718
/// {@macro handle_completion_request_command}
18-
HandleCompletionRequestCommand(this.logger);
19+
HandleCompletionRequestCommand();
1920

2021
@override
2122
String get description {
@@ -31,9 +32,6 @@ class HandleCompletionRequestCommand<T> extends Command<T> {
3132
@override
3233
bool get hidden => true;
3334

34-
/// The [Logger] used to display the completion suggestions
35-
final Logger logger;
36-
3735
@override
3836
CompletionCommandRunner<T> get runner {
3937
return super.runner! as CompletionCommandRunner<T>;
@@ -42,16 +40,38 @@ class HandleCompletionRequestCommand<T> extends Command<T> {
4240
@override
4341
FutureOr<T>? run() {
4442
try {
43+
// Get completion request params from the environment
4544
final completionState = CompletionState.fromEnvironment(
4645
runner.environmentOverride,
4746
);
47+
48+
// If the parameters in the environment are not supported or invalid,
49+
// do not proceed with completion complete.
4850
if (completionState == null) {
4951
return null;
5052
}
5153

52-
final result = CompletionParser(completionState).parse();
54+
// Find the completion level
55+
final completionLevel = CompletionLevel.find(
56+
completionState.args,
57+
runner.argParser,
58+
runner.commands,
59+
);
60+
61+
// Do not complete if the command structure is not recognized
62+
if (completionLevel == null) {
63+
return null;
64+
}
65+
66+
// Parse the completion level into completion suggestions
67+
final completionResults = CompletionParser(
68+
completionLevel: completionLevel,
69+
).parse();
5370

54-
runner.renderCompletionResult(result);
71+
// Render the completion suggestions
72+
for (final completionResult in completionResults) {
73+
runner.renderCompletionResult(completionResult);
74+
}
5575
} on Exception {
5676
// Do not output any Exception here, since even error messages are
5777
// interpreted as completion suggestions

lib/src/command_runner/completion_command_runner.dart

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import 'package:meta/meta.dart';
2323
abstract class CompletionCommandRunner<T> extends CommandRunner<T> {
2424
/// {@macro completion_command_runner}
2525
CompletionCommandRunner(super.executableName, super.description) {
26-
addCommand(HandleCompletionRequestCommand<T>(completionLogger));
26+
addCommand(HandleCompletionRequestCommand<T>());
2727
addCommand(InstallCompletionFilesCommand<T>());
2828
}
2929

@@ -93,6 +93,23 @@ abstract class CompletionCommandRunner<T> extends CommandRunner<T> {
9393
if (systemShell == null) {
9494
return;
9595
}
96-
completionResult.render(completionLogger, systemShell);
96+
97+
for (final entry in completionResult.completions.entries) {
98+
switch (systemShell) {
99+
case SystemShell.zsh:
100+
// On zsh, colon acts as delimitation between a suggestion and its
101+
// description. Any literal colon should be escaped.
102+
final suggestion = entry.key.replaceAll(':', r'\:');
103+
final description = entry.value?.replaceAll(':', r'\:');
104+
105+
completionLogger.info(
106+
'$suggestion${description != null ? ':$description' : ''}',
107+
);
108+
break;
109+
case SystemShell.bash:
110+
completionLogger.info(entry.key);
111+
break;
112+
}
113+
}
97114
}
98115
}

0 commit comments

Comments
 (0)