Skip to content

Commit dc63f35

Browse files
committed
1 parent 80bf37c commit dc63f35

File tree

11 files changed

+315
-88
lines changed

11 files changed

+315
-88
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## Unreleased
22

33
- Add `name` parameter to `Option` and `Flag` ( #102 )
4+
- Add option groups ( #40 )
45

56
## 0.6.1
67

README.md

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ main(arguments) => new Script(greet).execute(arguments);
2727
greet(
2828
@Rest(help: 'Name(s) to greet.')
2929
List<String> who, {
30+
@Group.start('Output');
3031
@Option(help: 'How many !\'s to append.')
3132
int enthusiasm : 0,
3233
@Flag(abbr: 'l', help: 'Put names on separate lines.')
@@ -61,28 +62,36 @@ $ greet.dart --help
6162

6263
Description:
6364

64-
Print a configurable greeting
65+
Print a configurable greeting.
6566

6667
Usage:
6768

68-
greet.dart [options] [WHO]...
69+
greet.dart [options] [<who>...]
70+
71+
<who> Name(s) to greet.
6972

7073
Options:
7174

72-
--salutation=<greeting> Alternate <greeting> to greet with e.g. "Hi".
73-
--enthusiasm How many !'s to append.
74-
-l, --line-mode Put names on separate lines.
75-
--completion Tab completion for this command.
75+
--completion Tab completion for this command.
76+
77+
[install] Install completion script to .bashrc/.zshrc.
78+
[print] Print completion script to stdout.
79+
[uninstall] Uninstall completion script from .bashrc/.zshrc.
80+
81+
-h, --help Print this usage information.
82+
83+
Output:
7684

77-
[install] Install completion script to .bashrc/.zshrc.
78-
[print] Print completion script to stdout.
79-
[uninstall] Uninstall completion script from .bashrc/.zshrc.
85+
--enthusiasm How many !'s to append.
86+
(defaults to "0")
8087
81-
-h, --help Print this usage information.
88+
-l, --line-mode Put names on separate lines.
89+
--greeting Alternate word to greet with e.g. "Hi".
90+
(defaults to "Hello")
8291
8392
Examples:
8493
85-
greet.dart --salutation Hi --enthusiasm 3 Bob # enthusiastic
94+
greet.dart --greeting Hi --enthusiasm 3 Bob # enthusiastic
8695
8796
```
8897

example/greet.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ main(arguments) => new Script(greet).execute(arguments);
66

77
// All metadata annotations are optional.
88
@Command(help: 'Print a configurable greeting.', plugins: const [const Completion()])
9-
@ArgExample('--salutation Hi --enthusiasm 3 Bob', help: 'enthusiastic')
9+
@ArgExample('--greeting Hi --enthusiasm 3 Bob', help: 'enthusiastic')
1010
greet(
1111
@Rest(valueHelp: 'who', help: 'Name(s) to greet.')
1212
List<String> who, {
13+
@Group.start(title: 'Output')
1314
@Option(help: 'How many !\'s to append.')
1415
int enthusiasm : 0,
1516
@Flag(abbr: 'l', help: 'Put names on separate lines.')

lib/src/annotations.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,33 @@ class SubCommand extends BaseCommand {
128128
const SubCommand({help, bool allowTrailingOptions, this.hide})
129129
: super(help: help, allowTrailingOptions: allowTrailingOptions);
130130
}
131+
132+
/// An annotation which defines a group of options.
133+
///
134+
/// The group of options will be separated from other options in help text,
135+
/// and may have a [title] and [help] as well.
136+
///
137+
/// See [new Group] and [new Group.start] for different ways to define groups.
138+
abstract class Group implements HelpAnnotation {
139+
140+
/// The title to display above the [help] for this group.
141+
String get title;
142+
143+
/// Whether to hide this group.
144+
bool get hide;
145+
146+
/// Marks a `Map<String, dynamic>` [Option] parameter as receiving values for
147+
/// multiple options as enumerated by [getOptions].
148+
const factory Group(
149+
Iterable<Option> getOptions(),
150+
{String title,
151+
help,
152+
bool hide}) = CombinedGroup;
153+
154+
/// Use [Group.start] to mark a `Map<String, dynamic>` [Option] parameter as starting a group which
155+
/// continues until the next [Group] parameter.
156+
const factory Group.start(
157+
{String title,
158+
help,
159+
bool hide}) = StartGroup;
160+
}

lib/src/group_annotations.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
import '../unscripted.dart';
3+
import 'util.dart';
4+
5+
class GroupMarker extends HelpAnnotation implements Group {
6+
7+
final String title;
8+
final bool hide;
9+
10+
const GroupMarker({
11+
this.title,
12+
help,
13+
this.hide})
14+
: super(help: help);
15+
}
16+
17+
class StartGroup extends GroupMarker {
18+
const StartGroup({
19+
String title,
20+
help,
21+
bool hide}) : super(title: title, help: help, hide: hide);
22+
}
23+
24+
class CombinedGroup extends GroupMarker {
25+
const CombinedGroup(
26+
this.getOptions,
27+
{String title,
28+
help,
29+
bool hide}) : super(title: title, help: help, hide: hide);
30+
31+
/// Whether to hide this group.
32+
final Function getOptions;
33+
}

lib/src/plugins/help/option_help.dart

Lines changed: 85 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ import 'util.dart';
2323
* spaces and wrapping to the next line to keep the cells correctly lined up.
2424
*/
2525
class OptionHelp {
26-
static const NUM_COLUMNS = 3; // Abbreviation, long name, help.
26+
static const numColumns = 3; // Abbreviation, long name, help.
2727

28-
/** The parser this is generating usage for. */
28+
static const gutterWidth = 4; // Width of gutter between columns.
29+
30+
/** The usage this is generating usage for. */
2931
final Usage usage;
3032

3133
/** The working buffer for the generated usage text. */
@@ -41,7 +43,7 @@ class OptionHelp {
4143
/** The width in characters of each column. */
4244
List<int> columnWidths;
4345

44-
/** The width in characters of each column. */
46+
/** The formatters of each column. */
4547
List<Function> columnFormatters = [abbrFormatter, optionPen, helpFormatter];
4648

4749
static String abbrFormatter(String help) {
@@ -86,47 +88,65 @@ class OptionHelp {
8688

8789
calculateColumnWidths();
8890

89-
usage.options.forEach((name, option) {
90-
if (option.hide != null && option.hide) return;
91+
usage.optionGroups.asMap().forEach((index, optionGroup) {
92+
if (optionGroup.hide != null && optionGroup.hide) return;
93+
94+
var title = optionGroup.title != null
95+
? getGroupTitle(optionGroup.title)
96+
: index == 0
97+
? null
98+
: '_' * (columnWidths.fold(0, (prev, next) => prev + next) - gutterWidth);
99+
if (title != null) {
100+
if (buffer.isNotEmpty) buffer.write("\n");
101+
write(0, title, format: titlePen);
102+
newline();
103+
newline();
104+
}
91105

92-
write(0, getAbbreviation(option));
93-
write(1, getLongOption(name, option));
106+
optionGroup.options.forEach((name, option) {
107+
if (option.hide != null && option.hide) return;
94108

95-
var help = getHelp(option.help);
96-
if (help != null) {
97-
write(2, help);
98-
}
109+
write(0, getAbbreviation(option));
110+
write(1, getLongOption(name, option));
99111

100-
if (option.allowed is Map) {
101-
var allowedValues = getAllowedValues(option).toList(growable: false);
102-
allowedValues.sort();
103-
newline();
104-
for (var name in allowedValues) {
105-
write(1, getAllowedTitle(name));
106-
write(2, option.allowed[name]);
112+
var help = getHelp(option.help);
113+
if (help != null) {
114+
write(2, help);
107115
}
108-
newline();
109-
} else if (getAllowedValues(option) != null) {
110-
write(2, buildAllowedList(option));
111-
} else if (option.defaultsTo != null) {
112-
var defaultsTo = option is Flag && option.defaultsTo == true ?
113-
'on' :
114-
option is! Flag ? option.defaultsTo : null;
115-
if(defaultsTo != null) {
116-
write(2, '(defaults to "$defaultsTo")');
116+
117+
if (option.allowed is Map) {
118+
var allowedValues = getAllowedValues(option).toList(growable: false);
119+
allowedValues.sort();
120+
newline();
121+
for (var name in allowedValues) {
122+
write(1, getAllowedTitle(name));
123+
write(2, option.allowed[name]);
124+
}
125+
newline();
126+
} else if (getAllowedValues(option) != null) {
127+
write(2, buildAllowedList(option));
128+
} else if (option.defaultsTo != null) {
129+
var defaultsTo = option is Flag && option.defaultsTo == true ?
130+
'on' :
131+
option is! Flag ? option.defaultsTo : null;
132+
if(defaultsTo != null) {
133+
write(2, '(defaults to "$defaultsTo")');
134+
}
117135
}
118-
}
119136

120-
// If any given option displays more than one line of text on the right
121-
// column (i.e. help, default value, allowed options, etc.) then put a
122-
// blank line after it. This gives space where it's useful while still
123-
// keeping simple one-line options clumped together.
124-
if (numHelpLines > 1) newline();
137+
// If any given option displays more than one line of text on the right
138+
// column (i.e. help, default value, allowed options, etc.) then put a
139+
// blank line after it. This gives space where it's useful while still
140+
// keeping simple one-line options clumped together.
141+
if (numHelpLines > 1) newline();
142+
});
125143
});
126144

127145
return buffer.toString();
128146
}
129147

148+
String getGroupTitle(String name) => '$name:';
149+
130150
Iterable<String> getAllowedValues(Option option) {
131151
var allowed = option.allowed;
132152
if(allowed is Iterable) return allowed;
@@ -164,23 +184,30 @@ class OptionHelp {
164184
void calculateColumnWidths() {
165185
int abbr = 0;
166186
int title = 0;
167-
usage.options.forEach((name, option) {
168-
// Make room in the first column if there are abbreviations.
169-
abbr = max(abbr, getAbbreviation(option).length);
187+
usage.optionGroups.forEach((optionGroup) {
188+
if (optionGroup.hide != null && optionGroup.hide) return;
189+
190+
// Make room for the group title.
191+
title = max(title, getGroupTitle(optionGroup.title).length);
170192

171-
// Make room for the option.
172-
title = max(title, getLongOption(name, option).length);
193+
optionGroup.options.forEach((name, option) {
194+
// Make room in the first column if there are abbreviations.
195+
abbr = max(abbr, getAbbreviation(option).length);
173196

174-
// Make room for the allowed help.
175-
if (option.allowed is Map) {
176-
for (var allowed in option.allowed.keys) {
177-
title = max(title, getAllowedTitle(allowed).length);
197+
// Make room for the option.
198+
title = max(title, getLongOption(name, option).length);
199+
200+
// Make room for the allowed help.
201+
if (option.allowed is Map) {
202+
for (var allowed in option.allowed.keys) {
203+
title = max(title, getAllowedTitle(allowed).length);
204+
}
178205
}
179-
}
206+
});
180207
});
181208

182209
// Leave a gutter between the columns.
183-
title += 4;
210+
title += gutterWidth;
184211
columnWidths = [abbr, title];
185212
}
186213

@@ -190,24 +217,24 @@ class OptionHelp {
190217
numHelpLines = 0;
191218
}
192219

193-
void write(int column, String text) {
220+
void write(int column, String text, {format(String)}) {
194221
var lines = text.split('\n');
195222

196223
// Strip leading and trailing empty lines.
197-
while (lines.length > 0 && lines[0].trim() == '') {
198-
lines.removeRange(0, 1);
224+
while (lines.isNotEmpty && lines.first.trim() == '') {
225+
lines.removeAt(0);
199226
}
200227

201-
while (lines.length > 0 && lines[lines.length - 1].trim() == '') {
228+
while (lines.isNotEmpty && lines.last.trim() == '') {
202229
lines.removeLast();
203230
}
204231

205232
for (var line in lines) {
206-
writeLine(column, line);
233+
writeLine(column, line, format: format);
207234
}
208235
}
209236

210-
void writeLine(int column, String text) {
237+
void writeLine(int column, String text, {format(String)}) {
211238
// Write any pending newlines.
212239
while (newlinesNeeded > 0) {
213240
buffer.write('\n');
@@ -217,15 +244,17 @@ class OptionHelp {
217244
// Advance until we are at the right column (which may mean wrapping around
218245
// to the next line.
219246
while (currentColumn != column) {
220-
if (currentColumn < NUM_COLUMNS - 1) {
247+
if (currentColumn < numColumns - 1) {
221248
buffer.write(' ' * columnWidths[currentColumn]);
222249
} else {
223250
buffer.write('\n');
224251
}
225-
currentColumn = (currentColumn + 1) % NUM_COLUMNS;
252+
currentColumn = (currentColumn + 1) % numColumns;
226253
}
227254

228-
var formatted = columnFormatters[column](text);
255+
var formatter = format != null ? format : columnFormatters[column];
256+
257+
var formatted = formatter(text);
229258
buffer.write(formatted);
230259

231260
if (column < columnWidths.length) {
@@ -234,14 +263,14 @@ class OptionHelp {
234263
}
235264

236265
// Advance to the next column.
237-
currentColumn = (currentColumn + 1) % NUM_COLUMNS;
266+
currentColumn = (currentColumn + 1) % numColumns;
238267

239268
// If we reached the last column, we need to wrap to the next line.
240-
if (column == NUM_COLUMNS - 1) newlinesNeeded++;
269+
if (column == numColumns - 1) newlinesNeeded++;
241270

242271
// Keep track of how many consecutive lines we've written in the last
243272
// column.
244-
if (column == NUM_COLUMNS - 1) {
273+
if (column == numColumns - 1) {
245274
numHelpLines++;
246275
} else {
247276
numHelpLines = 0;

0 commit comments

Comments
 (0)