| // 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; | |
| _currentColumn = 0; | |
| } | |
| 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(); | |
| } | |
| } |