Merge pull request #107 from dart-lang/prep-line-length
Prep line length
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d418df9..2a335e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.5.0
+
+* Add `usageLineLength` to control word wrapping usage text.
+
## 1.4.4
* Set max SDK version to `<3.0.0`, and adjust other dependencies.
diff --git a/lib/src/allow_anything_parser.dart b/lib/src/allow_anything_parser.dart
index 248a39a..31215ba 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 usageLineLength => 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..cdc54e6 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 usageLineLength;
+
/// 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 usageLineLength}) =>
new ArgParser._(<String, Option>{}, <String, ArgParser>{},
- allowTrailingOptions: allowTrailingOptions);
+ allowTrailingOptions: allowTrailingOptions,
+ usageLineLength: usageLineLength);
/// 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.usageLineLength})
: 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, lineLength: usageLineLength)
+ .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..8a1e764 100644
--- a/lib/src/usage.dart
+++ b/lib/src/usage.dart
@@ -2,7 +2,7 @@
// 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';
+import 'dart:math' as math;
import '../args.dart';
@@ -18,7 +18,8 @@
/// 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.
class Usage {
- static const NUM_COLUMNS = 3; // Abbreviation, long name, help.
+ /// Abbreviation, long name, help.
+ static const _columnCount = 3;
/// A list of the [Option]s intermingled with [String] separators.
final List optionsAndSeparators;
@@ -51,7 +52,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 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.
@@ -147,15 +153,15 @@
if (option.hide) continue;
// Make room in the first column if there are abbreviations.
- abbr = max(abbr, getAbbreviation(option).length);
+ abbr = math.max(abbr, getAbbreviation(option).length);
// Make room for the option.
- title = max(title, getLongOption(option).length);
+ title = math.max(title, getLongOption(option).length);
// Make room for the allowed help.
if (option.allowedHelp != null) {
for (var allowed in option.allowedHelp.keys) {
- title = max(title, getAllowedTitle(option, allowed).length);
+ title = math.max(title, getAllowedTitle(option, allowed).length);
}
}
}
@@ -171,8 +177,60 @@
numHelpLines = 0;
}
+ /// Wraps a single line of text into lines no longer than [lineLength],
+ /// starting at the [start] column.
+ ///
+ /// Tries to split at whitespace, but if that's not good enough to keep it
+ /// under the limit, then splits in the middle of a word.
+ List<String> _wrap(String text, int start) {
+ assert(lineLength != null, "Should wrap when given a length.");
+ assert(start >= 0);
+
+ text = text.trim();
+
+ var length = math.max(lineLength - start, 10);
+ if (text.length <= length) return [text];
+
+ var result = <String>[];
+ var currentLineStart = 0;
+ int lastWhitespace;
+ for (var 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) {
var lines = text.split('\n');
+ // If we are writing the last column, word wrap it to fit.
+ if (column == columnWidths.length && lineLength != null) {
+ var wrappedLines = <String>[];
+ var start = columnWidths
+ .sublist(0, column)
+ .reduce((start, width) => start += width);
+
+ for (var line in lines) {
+ wrappedLines.addAll(_wrap(line, start));
+ }
+
+ lines = wrappedLines;
+ }
// Strip leading and trailing empty lines.
while (lines.length > 0 && lines[0].trim() == '') {
@@ -198,31 +256,31 @@
// Advance until we are at the right column (which may mean wrapping around
// to the next line.
while (currentColumn != column) {
- if (currentColumn < NUM_COLUMNS - 1) {
- buffer.write(padRight('', columnWidths[currentColumn]));
+ if (currentColumn < _columnCount - 1) {
+ buffer.write(' ' * columnWidths[currentColumn]);
} else {
buffer.write('\n');
}
- currentColumn = (currentColumn + 1) % NUM_COLUMNS;
+ currentColumn = (currentColumn + 1) % _columnCount;
}
if (column < columnWidths.length) {
// Fixed-size column, so pad it.
- buffer.write(padRight(text, columnWidths[column]));
+ 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) % NUM_COLUMNS;
+ currentColumn = (currentColumn + 1) % _columnCount;
// If we reached the last column, we need to wrap to the next line.
- if (column == NUM_COLUMNS - 1) newlinesNeeded++;
+ if (column == _columnCount - 1) newlinesNeeded++;
// Keep track of how many consecutive lines we've written in the last
// column.
- if (column == NUM_COLUMNS - 1) {
+ if (column == _columnCount - 1) {
numHelpLines++;
} else {
numHelpLines = 0;
@@ -250,14 +308,22 @@
}
}
-/// Pads [source] to [length] by adding spaces at the end.
-String padRight(String source, int length) {
- final result = new StringBuffer();
- result.write(source);
-
- while (result.length < length) {
- result.write(' ');
- }
-
- return result.toString();
+/// Returns true if the code unit at [index] in [text] is a whitespace
+/// character.
+///
+/// Based on: https://en.wikipedia.org/wiki/Whitespace_character#Unicode
+bool _isWhitespace(String text, int index) {
+ var rune = text.codeUnitAt(index);
+ return rune >= 0x0009 && rune <= 0x000D ||
+ rune == 0x0020 ||
+ rune == 0x0085 ||
+ rune == 0x1680 ||
+ rune == 0x180E ||
+ rune >= 0x2000 && rune <= 0x200A ||
+ rune == 0x2028 ||
+ rune == 0x2029 ||
+ rune == 0x202F ||
+ rune == 0x205F ||
+ rune == 0x3000 ||
+ rune == 0xFEFF;
}
diff --git a/pubspec.yaml b/pubspec.yaml
index 80b6033..3f52f94 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: args
-version: 1.4.4
+version: 1.5.0
author: "Dart Team <misc@dartlang.org>"
homepage: https://github.com/dart-lang/args
description: >
diff --git a/test/usage_test.dart b/test/usage_test.dart
index 48a33dd..c556b6d 100644
--- a/test/usage_test.dart
+++ b/test/usage_test.dart
@@ -314,6 +314,126 @@
''');
});
+ test("help strings are not wrapped if usageLineLength is null", () {
+ var parser = new ArgParser(usageLineLength: 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 usageLineLength is specified",
+ () {
+ var parser = new ArgParser(usageLineLength: 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 usageLineLength is "
+ "smaller than available space", () {
+ var parser = new ArgParser(usageLineLength: 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('longWhitespace',
+ help:
+ ' The flag with a really long help text and whitespace at the start.');
+ parser.addFlag('longTrailspace',
+ help:
+ 'The flag with a really long help text and whitespace at the end. ');
+ 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-]longWhitespace The flag
+ with a
+ really
+ long help
+ text and
+ whitespace
+ at the
+ start.
+
+ --[no-]longTrailspace The flag
+ with a
+ really
+ long help
+ text and
+ whitespace
+ at the
+ end.
+
+ --[no-]small1 a
+ --[no-]small2 a
+ --[no-]small3 a
+ ''');
+ });
+
group("separators", () {
test("separates options where it's placed", () {
var parser = new ArgParser();