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()) =>