-
Notifications
You must be signed in to change notification settings - Fork 42
Expand file tree
/
Copy pathusage.dart
More file actions
253 lines (214 loc) · 7.9 KB
/
usage.dart
File metadata and controls
253 lines (214 loc) · 7.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:math' as math;
import '../args.dart';
import 'utils.dart';
/// Generates a string of usage (i.e. help) text for a list of options.
///
/// Internally, it works like a tabular printer. The output is divided into
/// three horizontal columns, like so:
///
/// -h, --help Prints the usage information
/// | | | |
///
/// It builds the usage text up one column at a time and handles padding with
/// spaces and wrapping to the next line to keep the cells correctly lined up.
///
/// [lineLength] specifies the horizontal character position at which the help
/// text is wrapped. Help that extends past this column will be wrapped at the
/// nearest whitespace (or truncated if there is no available whitespace). If
/// `null` there will not be any wrapping.
String generateUsage(List optionsAndSeparators, {int? lineLength}) =>
_Usage(optionsAndSeparators, lineLength).generate();
class _Usage {
/// Abbreviation, long name, help.
static const _columnCount = 3;
/// A list of the [Option]s intermingled with [String] separators.
final List _optionsAndSeparators;
/// The working buffer for the generated usage text.
final _buffer = StringBuffer();
/// The column that the "cursor" is currently on.
///
/// If the next call to [write()] is not for this column, it will correctly
/// handle advancing to the next column (and possibly the next row).
int _currentColumn = 0;
/// The width in characters of each column.
late final _columnWidths = _calculateColumnWidths();
/// How many newlines need to be rendered before the next bit of text can be
/// written.
///
/// We do this lazily so that the last bit of usage doesn't have dangling
/// newlines. We only write newlines right *before* we write some real
/// content.
int _newlinesNeeded = 0;
/// The horizontal character position at which help text is wrapped.
///
/// Help that extends past this column will be wrapped at the nearest
/// whitespace (or truncated if there is no available whitespace).
final int? lineLength;
_Usage(this._optionsAndSeparators, this.lineLength);
/// Generates a string displaying usage information for the defined options.
/// This is basically the help text shown on the command line.
String generate() {
for (var optionOrSeparator in _optionsAndSeparators) {
if (optionOrSeparator is String) {
_writeSeparator(optionOrSeparator);
continue;
}
var option = optionOrSeparator as Option;
if (option.hide) continue;
_writeOption(option);
}
return _buffer.toString();
}
void _writeSeparator(String separator) {
// Ensure that there's always a blank line before a separator.
if (_buffer.isNotEmpty) _buffer.write('\n\n');
_buffer.write(separator);
_newlinesNeeded = 1;
}
void _writeOption(Option option) {
_write(0, _abbreviation(option));
_write(1, '${_longOption(option)}${_mandatoryOption(option)}');
if (option.help case final help?) _write(2, help);
if (option.allowedHelp case final allowedHelp?) {
_newline();
for (var MapEntry(key: name, value: content) in allowedHelp.entries) {
_write(1, _allowedTitle(option, name));
_write(2, content);
}
_newline();
} else if (option.allowed != null) {
_write(2, _buildAllowedList(option));
} else if (option.isFlag) {
if (option.defaultsTo == true) {
_write(2, '(defaults to on)');
}
} else if (option.isMultiple) {
if (option.defaultsTo != null &&
(option.defaultsTo as Iterable).isNotEmpty) {
var defaults =
(option.defaultsTo as List).map((value) => '"$value"').join(', ');
_write(2, '(defaults to $defaults)');
}
} else if (option.defaultsTo != null) {
_write(2, '(defaults to "${option.defaultsTo}")');
}
}
String _abbreviation(Option option) =>
option.abbr == null ? '' : '-${option.abbr}, ';
String _longOption(Option option) {
String result;
if (option.negatable! && !option.hideNegatedUsage!) {
result = '--[no-]${option.name}';
} else {
result = '--${option.name}';
}
if (option.valueHelp != null) result += '=<${option.valueHelp}>';
return result;
}
String _mandatoryOption(Option option) {
return option.mandatory ? ' (mandatory)' : '';
}
String _allowedTitle(Option option, String allowed) {
var isDefault = option.defaultsTo is List
? (option.defaultsTo as List).contains(allowed)
: option.defaultsTo == allowed;
return ' [$allowed]${isDefault ? ' (default)' : ''}';
}
List<int> _calculateColumnWidths() {
var abbr = 0;
var title = 0;
for (var option in _optionsAndSeparators) {
if (option is! Option) continue;
if (option.hide) continue;
// Make room in the first column if there are abbreviations.
abbr = math.max(abbr, _abbreviation(option).length);
// Make room for the option.
title = math.max(
title, _longOption(option).length + _mandatoryOption(option).length);
// Make room for the allowed help.
if (option.allowedHelp != null) {
for (var allowed in option.allowedHelp!.keys) {
title = math.max(title, _allowedTitle(option, allowed).length);
}
}
}
// Leave a gutter between the columns.
title += 4;
return [abbr, title];
}
void _newline() {
_newlinesNeeded++;
_currentColumn = 0;
}
void _write(int column, String text) {
var lines = text.split('\n');
// If we are writing the last column, word wrap it to fit.
if (column == _columnWidths.length && lineLength != null) {
var start =
_columnWidths.take(column).reduce((start, width) => start + width);
lines = [
for (var line in lines)
...wrapTextAsLines(line, start: start, length: lineLength),
];
}
// Strip leading and trailing empty lines.
while (lines.isNotEmpty && lines.first.trim() == '') {
lines.removeAt(0);
}
while (lines.isNotEmpty && lines.last.trim() == '') {
lines.removeLast();
}
for (var line in lines) {
_writeLine(column, line);
}
}
void _writeLine(int column, String text) {
// Write any pending newlines.
while (_newlinesNeeded > 0) {
_buffer.write('\n');
_newlinesNeeded--;
}
// Advance until we are at the right column (which may mean wrapping around
// to the next line.
while (_currentColumn != column) {
if (_currentColumn < _columnCount - 1) {
_buffer.write(' ' * _columnWidths[_currentColumn]);
} else {
_buffer.write('\n');
}
_currentColumn = (_currentColumn + 1) % _columnCount;
}
if (column < _columnWidths.length) {
// Fixed-size column, so pad it.
_buffer.write(text.padRight(_columnWidths[column]));
} else {
// The last column, so just write it.
_buffer.write(text);
}
// Advance to the next column.
_currentColumn = (_currentColumn + 1) % _columnCount;
// If we reached the last column, we need to wrap to the next line.
if (column == _columnCount - 1) _newlinesNeeded++;
}
String _buildAllowedList(Option option) {
var isDefault = option.defaultsTo is List
? (option.defaultsTo as List).contains
: (String value) => value == option.defaultsTo;
var allowedBuffer = StringBuffer();
allowedBuffer.write('[');
var first = true;
for (var allowed in option.allowed!) {
if (!first) allowedBuffer.write(', ');
allowedBuffer.write(allowed);
if (isDefault(allowed)) {
allowedBuffer.write(' (default)');
}
first = false;
}
allowedBuffer.write(']');
return allowedBuffer.toString();
}
}