Provide access to ansi values suitable for shell scripts Required for https://github.com/dart-lang/mono_repo/issues/43
diff --git a/CHANGELOG.md b/CHANGELOG.md index c2565ea..5a26062 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md
@@ -1,3 +1,15 @@ +## 0.3.2 + +* `ansi.dart` + + * Added `forScript` named argument to top-level `wrapWith` function. + + * `AnsiCode` + + * Added `String get escapeForScript` property. + + * Added `forScript` named argument to `wrap` function. + ## 0.3.1 - Added `SharedStdIn.nextLine` (similar to `readLineSync`) and `lines`:
diff --git a/example/ansi_code_example.dart b/example/ansi_code_example.dart index 255bd72..e622f03 100644 --- a/example/ansi_code_example.dart +++ b/example/ansi_code_example.dart
@@ -7,17 +7,19 @@ import 'package:io/ansi.dart'; /// Prints a sample of all of the `AnsiCode` values. -void main() { +void main(List<String> args) { + final forScript = args.contains('--for-script'); + if (!ansiOutputEnabled) { print('`ansiOutputEnabled` is `false`.'); print("Don't expect pretty output."); } - _preview('Foreground', foregroundColors); - _preview('Background', backgroundColors); - _preview('Styles', styles); + _preview('Foreground', foregroundColors, forScript); + _preview('Background', backgroundColors, forScript); + _preview('Styles', styles, forScript); } -void _preview(String name, List<AnsiCode> values) { +void _preview(String name, List<AnsiCode> values, bool forScript) { print(''); final longest = values.map((ac) => ac.name.length).reduce(max); @@ -26,6 +28,6 @@ final header = "${code.name.padRight(longest)} ${code.code.toString().padLeft(3)}"; - print("$header: ${code.wrap('Sample')}"); + print("$header: ${code.wrap('Sample', forScript: forScript)}"); } }
diff --git a/lib/src/ansi_code.dart b/lib/src/ansi_code.dart index beee945..87551ee 100644 --- a/lib/src/ansi_code.dart +++ b/lib/src/ansi_code.dart
@@ -5,6 +5,9 @@ import 'dart:async'; import 'dart:io' as io; +const _ansiEscapeLiteral = '\x1B'; +const _ansiEscapeForScript = '\\033'; + /// Whether formatted ANSI output is enabled for [wrapWith] and [AnsiCode.wrap]. /// /// By default, returns `true` if both `stdout.supportsAnsiEscapes` and @@ -66,20 +69,31 @@ const AnsiCode._(this.name, this.type, this.code, this.reset); /// Represents the value escaped for use in terminal output. - String get escape => "\x1B[${code}m"; + String get escape => "$_ansiEscapeLiteral[${code}m"; + + /// Represents the value as an unescaped literal suitable for scripts. + String get escapeForScript => "$_ansiEscapeForScript[${code}m"; + + String _escapeValue({bool forScript: false}) { + forScript ??= false; + return forScript ? escapeForScript : escape; + } /// Wraps [value] with the [escape] value for this code, followed by /// [resetAll]. /// + /// If [forScript] is `true`, the return value is an unescaped literal. + /// /// Returns `value` unchanged if /// * [value] is `null` or empty /// * [ansiOutputEnabled] is `false` /// * [type] is [AnsiCodeType.reset] - String wrap(String value) => (ansiOutputEnabled && + String wrap(String value, {bool forScript: false}) => (ansiOutputEnabled && type != AnsiCodeType.reset && value != null && value.isNotEmpty) - ? "$escape$value${reset.escape}" + ? "${_escapeValue(forScript: forScript)}$value" + "${reset._escapeValue(forScript: forScript)}" : value; @override @@ -88,6 +102,8 @@ /// Returns a [String] formatted with [codes]. /// +/// If [forScript] is `true`, the return value is an unescaped literal. +/// /// Returns `value` unchanged if /// * [value] is `null` or empty. /// * [ansiOutputEnabled] is `false`. @@ -97,7 +113,9 @@ /// * [codes] contains more than one value of type [AnsiCodeType.foreground]. /// * [codes] contains more than one value of type [AnsiCodeType.background]. /// * [codes] contains any value of type [AnsiCodeType.reset]. -String wrapWith(String value, Iterable<AnsiCode> codes) { +String wrapWith(String value, Iterable<AnsiCode> codes, + {bool forScript: false}) { + forScript ??= false; // Eliminate duplicates final myCodes = codes.toSet(); @@ -130,8 +148,10 @@ } final sortedCodes = myCodes.map((ac) => ac.code).toList()..sort(); + final escapeValue = forScript ? _ansiEscapeForScript : _ansiEscapeLiteral; - return "\x1B[${sortedCodes.join(';')}m$value${resetAll.escape}"; + return "$escapeValue[${sortedCodes.join(';')}m$value" + "${resetAll._escapeValue(forScript: forScript)}"; } //
diff --git a/pubspec.yaml b/pubspec.yaml index 78ac7de..36a714d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml
@@ -1,7 +1,7 @@ name: io description: > Utilities for the Dart VM Runtime. -version: 0.3.1 +version: 0.3.2 author: Dart Team <misc@dartlang.org> homepage: https://github.com/dart-lang/io
diff --git a/test/ansi_code_test.dart b/test/ansi_code_test.dart index 89b2512..5c6805a 100644 --- a/test/ansi_code_test.dart +++ b/test/ansi_code_test.dart
@@ -7,6 +7,9 @@ import 'package:io/ansi.dart'; import 'package:test/test.dart'; +const _ansiEscapeLiteral = '\x1B'; +const _ansiEscapeForScript = '\\033'; + void main() { group('ansiOutputEnabled', () { test("default value matches dart:io", () { @@ -63,84 +66,106 @@ final sampleInput = 'sample input'; - group('wrap', () { - _test("color", () { - final expected = '\x1B[34m$sampleInput\x1B[0m'; + for (var forScript in [true, false]) { + group(forScript ? 'for script' : 'literal', () { + final escapeLiteral = + forScript ? _ansiEscapeForScript : _ansiEscapeLiteral; - expect(blue.wrap(sampleInput), expected); + group('wrap', () { + _test("color", () { + final expected = '$escapeLiteral[34m$sampleInput$escapeLiteral[0m'; + + expect(blue.wrap(sampleInput, forScript: forScript), expected); + }); + + _test("style", () { + final expected = '$escapeLiteral[1m$sampleInput$escapeLiteral[22m'; + + expect(styleBold.wrap(sampleInput, forScript: forScript), expected); + }); + + _test("style", () { + final expected = '$escapeLiteral[34m$sampleInput$escapeLiteral[0m'; + + expect(blue.wrap(sampleInput, forScript: forScript), expected); + }); + + test("empty", () { + expect(blue.wrap('', forScript: forScript), ''); + }); + + test(null, () { + expect(blue.wrap(null, forScript: forScript), isNull); + }); + }); + + group('wrapWith', () { + _test("foreground", () { + final expected = '$escapeLiteral[34m$sampleInput$escapeLiteral[0m'; + + expect(wrapWith(sampleInput, [blue], forScript: forScript), expected); + }); + + _test("background", () { + final expected = '$escapeLiteral[44m$sampleInput$escapeLiteral[0m'; + + expect(wrapWith(sampleInput, [backgroundBlue], forScript: forScript), + expected); + }); + + _test("style", () { + final expected = '$escapeLiteral[1m$sampleInput$escapeLiteral[0m'; + + expect(wrapWith(sampleInput, [styleBold], forScript: forScript), + expected); + }); + + _test("2 styles", () { + final expected = '$escapeLiteral[1;3m$sampleInput$escapeLiteral[0m'; + + expect( + wrapWith(sampleInput, [styleBold, styleItalic], + forScript: forScript), + expected); + }); + + _test("2 foregrounds", () { + expect( + () => wrapWith(sampleInput, [blue, white], forScript: forScript), + throwsArgumentError); + }); + + _test("multi", () { + final expected = + '$escapeLiteral[1;4;34;107m$sampleInput$escapeLiteral[0m'; + + expect( + wrapWith(sampleInput, + [blue, backgroundWhite, styleBold, styleUnderlined], + forScript: forScript), + expected); + }); + + test('no codes', () { + expect(wrapWith(sampleInput, []), sampleInput); + }); + + _test("empty", () { + expect( + wrapWith('', [blue, backgroundWhite, styleBold], + forScript: forScript), + ''); + }); + + _test(null, () { + expect( + wrapWith(null, [blue, backgroundWhite, styleBold], + forScript: forScript), + isNull); + }); + }); }); - - _test("style", () { - final expected = '\x1B[1m$sampleInput\x1B[22m'; - - expect(styleBold.wrap(sampleInput), expected); - }); - - _test("style", () { - final expected = '\x1B[34m$sampleInput\x1B[0m'; - - expect(blue.wrap(sampleInput), expected); - }); - - test("empty", () { - expect(blue.wrap(''), ''); - }); - - test(null, () { - expect(blue.wrap(null), isNull); - }); - }); - - group('wrapWith', () { - _test("foreground", () { - final expected = '\x1B[34m$sampleInput\x1B[0m'; - - expect(wrapWith(sampleInput, [blue]), expected); - }); - - _test("background", () { - final expected = '\x1B[44m$sampleInput\x1B[0m'; - - expect(wrapWith(sampleInput, [backgroundBlue]), expected); - }); - - _test("style", () { - final expected = '\x1B[1m$sampleInput\x1B[0m'; - - expect(wrapWith(sampleInput, [styleBold]), expected); - }); - - _test("2 styles", () { - final expected = '\x1B[1;3m$sampleInput\x1B[0m'; - - expect(wrapWith(sampleInput, [styleBold, styleItalic]), expected); - }); - - _test("2 foregrounds", () { - expect(() => wrapWith(sampleInput, [blue, white]), throwsArgumentError); - }); - - _test("multi", () { - final expected = '\x1B[1;4;34;107m$sampleInput\x1B[0m'; - - expect( - wrapWith( - sampleInput, [blue, backgroundWhite, styleBold, styleUnderlined]), - expected); - }); - - test('no codes', () { - expect(wrapWith(sampleInput, []), sampleInput); - }); - - _test("empty", () { - expect(wrapWith('', [blue, backgroundWhite, styleBold]), ''); - }); - - _test(null, () { - expect(wrapWith(null, [blue, backgroundWhite, styleBold]), isNull); - }); - }); + } } void _test<T>(String name, T body()) =>