Add optional wrapping of text for usage messages.
diff --git a/lib/src/allow_anything_parser.dart b/lib/src/allow_anything_parser.dart
index 248a39a..34a0b19 100644
--- a/lib/src/allow_anything_parser.dart
+++ b/lib/src/allow_anything_parser.dart
@@ -13,6 +13,7 @@
Map<String, ArgParser> get commands => const {};
bool get allowTrailingOptions => false;
bool get allowsAnything => true;
+ int get maxLineLength => null;
ArgParser addCommand(String name, [ArgParser parser]) {
throw new UnsupportedError(
diff --git a/lib/src/arg_parser.dart b/lib/src/arg_parser.dart
index bcdb8df..a25a8b8 100644
--- a/lib/src/arg_parser.dart
+++ b/lib/src/arg_parser.dart
@@ -30,6 +30,18 @@
/// arguments.
final bool allowTrailingOptions;
+ /// An optional maximum line length for [usage] messages.
+ ///
+ /// If specified, then help messages in the usage are wrapped at the given
+ /// column, after taking into account the width of the options. Will refuse to
+ /// wrap help text to less than 10 characters of help text per line if there
+ /// isn't enough space on the line. It preserves embedded newlines, and
+ /// attempts to wrap at whitespace breaks (although it will split words if
+ /// there is no whitespace at which to split).
+ ///
+ /// If null (the default), help messages are not wrapped.
+ final int maxLineLength;
+
/// Whether or not this parser treats unrecognized options as non-option
/// arguments.
bool get allowsAnything => false;
@@ -40,9 +52,10 @@
/// flags and options that appear after positional arguments. If it's `false`,
/// the parser stops parsing as soon as it finds an argument that is neither
/// an option nor a command.
- factory ArgParser({bool allowTrailingOptions: true}) =>
+ factory ArgParser({bool allowTrailingOptions: true, int maxLineLength}) =>
new ArgParser._(<String, Option>{}, <String, ArgParser>{},
- allowTrailingOptions: allowTrailingOptions);
+ allowTrailingOptions: allowTrailingOptions,
+ maxLineLength: maxLineLength);
/// Creates a new ArgParser that treats *all input* as non-option arguments.
///
@@ -53,7 +66,7 @@
factory ArgParser.allowAnything() = AllowAnythingParser;
ArgParser._(Map<String, Option> options, Map<String, ArgParser> commands,
- {bool allowTrailingOptions: true})
+ {bool allowTrailingOptions: true, this.maxLineLength})
: this._options = options,
this.options = new UnmodifiableMapView(options),
this._commands = commands,
@@ -315,7 +328,10 @@
/// Generates a string displaying usage information for the defined options.
///
/// This is basically the help text shown on the command line.
- String get usage => new Usage(_optionsAndSeparators).generate();
+ String get usage {
+ return new Usage(_optionsAndSeparators, maxLineLength: maxLineLength)
+ .generate();
+ }
/// Get the default value for an option. Useful after parsing to test if the
/// user specified something other than the default.
diff --git a/lib/src/usage.dart b/lib/src/usage.dart
index aa3ed07..9898b44 100644
--- a/lib/src/usage.dart
+++ b/lib/src/usage.dart
@@ -51,7 +51,12 @@
/// content.
int newlinesNeeded = 0;
- Usage(this.optionsAndSeparators);
+ /// 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 maxLineLength;
+
+ Usage(this.optionsAndSeparators, {this.maxLineLength});
/// Generates a string displaying usage information for the defined options.
/// This is basically the help text shown on the command line.
@@ -171,8 +176,52 @@
numHelpLines = 0;
}
+ /// Wrap a single line of text into lines no longer than length.
+ /// Try to split at whitespace, but if that's not good enough to keep it
+ /// under the length, then split in the middle of a word.
+ List<String> wrap(String text, int length) {
+ assert(length > 0, 'Wrap length must be larger than zero.');
+ text = text.trim();
+ if (text.length <= length) {
+ return [text];
+ }
+ var result = <String>[];
+ int currentLineStart = 0;
+ int lastWhitespace;
+ for (int i = 0; i < text.length; ++i) {
+ if (isWhitespace(text, i)) {
+ lastWhitespace = i;
+ }
+ if ((i - currentLineStart) >= length) {
+ // Back up to the last whitespace, unless there wasn't any, in which
+ // case we just split where we are.
+ if (lastWhitespace != null) {
+ i = lastWhitespace;
+ }
+ result.add(text.substring(currentLineStart, i));
+ // Skip any intervening whitespace.
+ while (isWhitespace(text, i) && i < text.length) i++;
+ currentLineStart = i;
+ lastWhitespace = null;
+ }
+ }
+ result.add(text.substring(currentLineStart));
+ return result;
+ }
+
void write(int column, String text) {
+ const int minColumnWidth = 10;
var lines = text.split('\n');
+ if (column == columnWidths.length && maxLineLength != null) {
+ var wrappedLines = <String>[];
+ var start = 0;
+ columnWidths.sublist(0, column).forEach((int width) => start += width);
+ for (var line in lines) {
+ wrappedLines
+ .addAll(wrap(line, max(maxLineLength - start, minColumnWidth)));
+ }
+ lines = wrappedLines;
+ }
// Strip leading and trailing empty lines.
while (lines.length > 0 && lines[0].trim() == '') {
@@ -261,3 +310,20 @@
return result.toString();
}
+
+bool isWhitespace(String text, int index) {
+ final int rune = text.codeUnitAt(index);
+ return ((rune >= 0x0009 && rune <= 0x000D) ||
+ rune == 0x0020 ||
+ rune == 0x0085 ||
+ rune == 0x00A0 ||
+ rune == 0x1680 ||
+ rune == 0x180E ||
+ (rune >= 0x2000 && rune <= 0x200A) ||
+ rune == 0x2028 ||
+ rune == 0x2029 ||
+ rune == 0x202F ||
+ rune == 0x205F ||
+ rune == 0x3000 ||
+ rune == 0xFEFF);
+}
diff --git a/test/usage_test.dart b/test/usage_test.dart
index 48a33dd..3bd228c 100644
--- a/test/usage_test.dart
+++ b/test/usage_test.dart
@@ -314,6 +314,102 @@
''');
});
+ test("help strings are not wrapped if maxLineLength is null", () {
+ var parser = new ArgParser(maxLineLength: null);
+ parser.addFlag('long',
+ help: 'The flag with a really long help text that will not '
+ 'be wrapped.');
+ validateUsage(parser, '''
+ --[no-]long The flag with a really long help text that will not be wrapped.
+ ''');
+ });
+
+ test("help strings are wrapped properly when maxLineLength is specified",
+ () {
+ var parser = new ArgParser(maxLineLength: 60);
+ parser.addFlag('long',
+ help: 'The flag with a really long help text that will be wrapped.');
+ parser.addFlag('longNewline',
+ help: 'The flag with a really long help text and newlines\n\nthat '
+ 'will still be wrapped because it is really long.');
+ parser.addFlag('solid',
+ help:
+ 'The-flag-with-no-whitespace-that-will-be-wrapped-by-splitting-a-word.');
+ parser.addFlag('small1', help: ' a ');
+ parser.addFlag('small2', help: ' a');
+ parser.addFlag('small3', help: 'a ');
+ validateUsage(parser, '''
+ --[no-]long The flag with a really long help text
+ that will be wrapped.
+
+ --[no-]longNewline The flag with a really long help text
+ and newlines
+
+ that will still be wrapped because it
+ is really long.
+
+ --[no-]solid The-flag-with-no-whitespace-that-will-
+ be-wrapped-by-splitting-a-word.
+
+ --[no-]small1 a
+ --[no-]small2 a
+ --[no-]small3 a
+ ''');
+ });
+
+ test(
+ "help strings are wrapped with at 10 chars when maxHelpLineLength is "
+ "smaller than available space", () {
+ var parser = new ArgParser(maxLineLength: 1);
+ parser.addFlag('long',
+ help: 'The flag with a really long help text that will be wrapped.');
+ parser.addFlag('longNewline',
+ help:
+ 'The flag with a really long help text and newlines\n\nthat will '
+ 'still be wrapped because it is really long.');
+ parser.addFlag('solid',
+ help:
+ 'The-flag-with-no-whitespace-that-will-be-wrapped-by-splitting-a-word.');
+ parser.addFlag('small1', help: ' a ');
+ parser.addFlag('small2', help: ' a');
+ parser.addFlag('small3', help: 'a ');
+ validateUsage(parser, '''
+ --[no-]long The flag
+ with a
+ really
+ long help
+ text that
+ will be
+ wrapped.
+
+ --[no-]longNewline The flag
+ with a
+ really
+ long help
+ text and
+ newlines
+
+ that will
+ still be
+ wrapped
+ because it
+ is really
+ long.
+
+ --[no-]solid The-flag-w
+ ith-no-whi
+ tespace-th
+ at-will-be
+ -wrapped-b
+ y-splittin
+ g-a-word.
+
+ --[no-]small1 a
+ --[no-]small2 a
+ --[no-]small3 a
+ ''');
+ });
+
group("separators", () {
test("separates options where it's placed", () {
var parser = new ArgParser();