Merge pull request #1209 from dart-lang/merge-package_config-package
Merge `package:package_config`
diff --git a/.github/ISSUE_TEMPLATE/io.md b/.github/ISSUE_TEMPLATE/io.md
new file mode 100644
index 0000000..5646f0f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/io.md
@@ -0,0 +1,5 @@
+---
+name: "package:io"
+about: "Create a bug or file a feature request against package:io."
+labels: "package:io"
+---
\ No newline at end of file
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 31b8b47..39d78aa 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -64,6 +64,10 @@
- changed-files:
- any-glob-to-any-file: 'pkgs/html/**'
+'package:io':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/io/**'
+
'package:json_rpc_2':
- changed-files:
- any-glob-to-any-file: 'pkgs/json_rpc_2/**'
diff --git a/.github/workflows/io.yaml b/.github/workflows/io.yaml
new file mode 100644
index 0000000..0c719a6
--- /dev/null
+++ b/.github/workflows/io.yaml
@@ -0,0 +1,72 @@
+name: package:io
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/io.yml'
+ - 'pkgs/io/**'
+ pull_request:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/io.yml'
+ - 'pkgs/io/**'
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+ run:
+ working-directory: pkgs/io/
+
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev and stable.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev, 3.4]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev, stable
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Add macos-latest and/or windows-latest if relevant for this package.
+ os: [ubuntu-latest]
+ sdk: [dev, 3.4]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - run: dart test
+ if: always() && steps.install.outcome == 'success'
diff --git a/README.md b/README.md
index 01360e9..0acab2c 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,7 @@
| [file_testing](pkgs/file_testing/) | Testing utilities for package:file. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Afile_testing) | [](https://pub.dev/packages/file_testing) |
| [graphs](pkgs/graphs/) | Graph algorithms that operate on graphs in any representation. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Agraphs) | [](https://pub.dev/packages/graphs) |
| [html](pkgs/html/) | APIs for parsing and manipulating HTML content outside the browser. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Ahtml) | [](https://pub.dev/packages/html) |
+| [io](pkgs/io/) | Utilities for the Dart VM Runtime including support for ANSI colors, file copying, and standard exit code values. | [](https://pub.dev/packages/io) |
| [json_rpc_2](pkgs/json_rpc_2/) | Utilities to write a client or server using the JSON-RPC 2.0 spec. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Ajson_rpc_2) | [](https://pub.dev/packages/json_rpc_2) |
| [mime](pkgs/mime/) | Utilities for handling media (MIME) types, including determining a type from a file extension and file contents. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Amime) | [](https://pub.dev/packages/mime) |
| [oauth2](pkgs/oauth2/) | A client library for authenticating with a remote service via OAuth2 on behalf of a user, and making authorized HTTP requests with the user's OAuth2 credentials. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aoauth2) | [](https://pub.dev/packages/oauth2) |
diff --git a/pkgs/io/.gitignore b/pkgs/io/.gitignore
new file mode 100644
index 0000000..01d42c0
--- /dev/null
+++ b/pkgs/io/.gitignore
@@ -0,0 +1,4 @@
+.dart_tool/
+.pub/
+.packages
+pubspec.lock
diff --git a/pkgs/io/AUTHORS b/pkgs/io/AUTHORS
new file mode 100644
index 0000000..ff09364
--- /dev/null
+++ b/pkgs/io/AUTHORS
@@ -0,0 +1,7 @@
+# Below is a list of people and organizations that have contributed
+# to the project. Names should be added to the list like so:
+#
+# Name/Organization <email address>
+
+Google Inc.
+
diff --git a/pkgs/io/CHANGELOG.md b/pkgs/io/CHANGELOG.md
new file mode 100644
index 0000000..e0631fa
--- /dev/null
+++ b/pkgs/io/CHANGELOG.md
@@ -0,0 +1,119 @@
+## 1.0.5
+
+* Require Dart 3.4.
+* Move to `dart-lang/tools` monorepo.
+
+## 1.0.4
+
+* Updates to the readme.
+
+## 1.0.3
+
+* Revert `meta` constraint to `^1.3.0`.
+
+## 1.0.2
+
+* Update `meta` constraint to `>=1.3.0 <3.0.0`.
+
+## 1.0.1
+
+* Update code examples to call the unified `dart` developer tool.
+
+## 1.0.0
+
+* Migrate this package to null-safety.
+* Require Dart >=2.12.
+
+## 0.3.5
+
+* Require Dart >=2.1.
+* Remove dependency on `package:charcode`.
+
+## 0.3.4
+
+* Fix a number of issues affecting the package score on `pub.dev`.
+
+## 0.3.3
+
+* Updates for Dart 2 constants. Require at least Dart `2.0.0-dev.54`.
+
+* Fix the type of `StartProcess` typedef to match `Process.start` from
+ `dart:io`.
+
+## 0.3.2+1
+
+* `ansi.dart`
+
+ * The "forScript" code paths now ignore the `ansiOutputEnabled` value. Affects
+ the `escapeForScript` property on `AnsiCode` and the `wrap` and `wrapWith`
+ functions when `forScript` is true.
+
+## 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`:
+
+```dart
+main() async {
+ // Prints the first line entered on stdin.
+ print(await sharedStdIn.nextLine());
+
+ // Prints all remaining lines.
+ await for (final line in sharedStdIn.lines) {
+ print(line);
+ }
+}
+```
+
+- Added a `copyPath` and `copyPathSync` function, similar to `cp -R`.
+
+- Added a dependency on `package:path`.
+
+- Added the remaining missing arguments to `ProcessManager.spawnX` which
+ forward to `Process.start`. It is now an interchangeable function for running
+ a process.
+
+## 0.3.0
+
+- **BREAKING CHANGE**: The `arguments` argument to `ProcessManager.spawn` is
+ now positional (not named) and required. This makes it more similar to the
+ built-in `Process.start`, and easier to use as a drop in replacement:
+
+```dart
+main() {
+ processManager.spawn('dart', ['--version']);
+}
+```
+
+- Fixed a bug where processes created from `ProcessManager.spawn` could not
+ have their `stdout`/`stderr` read through their respective getters (a runtime
+ error was always thrown).
+
+- Added `ProcessMangaer#spawnBackground`, which does not forward `stdin`.
+
+- Added `ProcessManager#spawnDetached`, which does not forward any I/O.
+
+- Added the `shellSplit()` function, which parses a list of arguments in the
+ same manner as [the POSIX shell][what_is_posix_shell].
+
+[what_is_posix_shell]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/contents.html
+
+## 0.2.0
+
+- Initial commit of...
+ - `FutureOr<bool> String isExecutable(path)`.
+ - `ExitCode`
+ - `ProcessManager` and `Spawn`
+ - `sharedStdIn` and `SharedStdIn`
+ - `ansi.dart` library with support for formatting terminal output
diff --git a/pkgs/io/LICENSE b/pkgs/io/LICENSE
new file mode 100644
index 0000000..03af64a
--- /dev/null
+++ b/pkgs/io/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2017, the Dart project authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google LLC nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/io/README.md b/pkgs/io/README.md
new file mode 100644
index 0000000..adbc941
--- /dev/null
+++ b/pkgs/io/README.md
@@ -0,0 +1,104 @@
+[](https://github.com/dart-lang/tools/actions/workflows/io.yaml)
+[](https://pub.dev/packages/io)
+[](https://pub.dev/packages/io/publisher)
+
+Contains utilities for the Dart VM's `dart:io`.
+
+## Usage - `io.dart`
+
+### Files
+
+#### `isExecutable`
+
+Returns whether a provided file path is considered _executable_ on the host
+operating system.
+
+### Processes
+
+#### `ExitCode`
+
+An `enum`-like class that contains known exit codes.
+
+#### `ProcessManager`
+
+A higher-level service for spawning and communicating with processes.
+
+##### Use `spawn` to create a process with std[in|out|err] forwarded by default
+
+```dart
+Future<void> main() async {
+ final manager = ProcessManager();
+
+ // Print `dart` tool version to stdout.
+ print('** Running `dart --version`');
+ var spawn = await manager.spawn('dart', ['--version']);
+ await spawn.exitCode;
+
+ // Check formatting and print the result to stdout.
+ print('** Running `dart format --output=none .`');
+ spawn = await manager.spawn('dart', ['format', '--output=none', '.']);
+ await spawn.exitCode;
+
+ // Check if a package is ready for publishing.
+ // Upon hitting a blocking stdin state, you may directly
+ // output to the processes's stdin via your own, similar to how a bash or
+ // shell script would spawn a process.
+ print('** Running pub publish');
+ spawn = await manager.spawn('dart', ['pub', 'publish', '--dry-run']);
+ await spawn.exitCode;
+
+ // Closes stdin for the entire program.
+ await sharedStdIn.terminate();
+}
+```
+
+#### `sharedStdIn`
+
+A safer version of the default `stdin` stream from `dart:io` that allows a
+subscriber to cancel their subscription, and then allows a _new_ subscriber to
+start listening. This differs from the default behavior where only a single
+listener is ever allowed in the application lifecycle:
+
+```dart
+test('should allow multiple subscribers', () async {
+ final logs = <String>[];
+ final asUtf8 = sharedStdIn.transform(UTF8.decoder);
+ // Wait for input for the user.
+ logs.add(await asUtf8.first);
+ // Wait for more input for the user.
+ logs.add(await asUtf8.first);
+ expect(logs, ['Hello World', 'Goodbye World']);
+});
+```
+
+For testing, an instance of `SharedStdIn` may be created directly.
+
+## Usage - `ansi.dart`
+
+```dart
+import 'dart:io' as io;
+import 'package:io/ansi.dart';
+
+void main() {
+ // To use one style, call the `wrap` method on one of the provided top-level
+ // values.
+ io.stderr.writeln(red.wrap("Bad error!"));
+
+ // To use multiple styles, call `wrapWith`.
+ print(wrapWith('** Important **', [red, styleBold, styleUnderlined]));
+
+ // The wrap functions will simply return the provided value unchanged if
+ // `ansiOutputEnabled` is false.
+ //
+ // You can override the value `ansiOutputEnabled` by wrapping code in
+ // `overrideAnsiOutput`.
+ overrideAnsiOutput(false, () {
+ assert('Normal text' == green.wrap('Normal text'));
+ });
+}
+```
+
+## Publishing automation
+
+For information about our publishing automation and release process, see
+https://github.com/dart-lang/ecosystem/wiki/Publishing-automation.
diff --git a/pkgs/io/analysis_options.yaml b/pkgs/io/analysis_options.yaml
new file mode 100644
index 0000000..6d74ee9
--- /dev/null
+++ b/pkgs/io/analysis_options.yaml
@@ -0,0 +1,32 @@
+# https://dart.dev/guides/language/analysis-options
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+ language:
+ strict-casts: true
+ strict-inference: true
+ strict-raw-types: true
+
+linter:
+ rules:
+ - avoid_bool_literals_in_conditional_expressions
+ - avoid_classes_with_only_static_members
+ - avoid_private_typedef_functions
+ - avoid_redundant_argument_values
+ - avoid_returning_this
+ - avoid_unused_constructor_parameters
+ - avoid_void_async
+ - cancel_subscriptions
+ - join_return_with_assignment
+ - literal_only_boolean_expressions
+ - missing_whitespace_between_adjacent_strings
+ - no_adjacent_strings_in_list
+ - no_runtimeType_toString
+ - prefer_const_declarations
+ - prefer_expression_function_bodies
+ - prefer_final_locals
+ - unnecessary_await_in_return
+ - unnecessary_breaks
+ - use_if_null_to_convert_nulls_to_bools
+ - use_raw_strings
+ - use_string_buffers
diff --git a/pkgs/io/example/example.dart b/pkgs/io/example/example.dart
new file mode 100644
index 0000000..8e358fd
--- /dev/null
+++ b/pkgs/io/example/example.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2017, 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';
+
+import 'package:io/ansi.dart';
+
+/// Prints a sample of all of the `AnsiCode` values.
+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, forScript);
+ _preview('Background', backgroundColors, forScript);
+ _preview('Styles', styles, forScript);
+}
+
+void _preview(String name, List<AnsiCode> values, bool forScript) {
+ print('');
+ final longest = values.map((ac) => ac.name.length).reduce(max);
+
+ print(wrapWith('** $name **', [styleBold, styleUnderlined]));
+ for (var code in values) {
+ final header =
+ '${code.name.padRight(longest)} ${code.code.toString().padLeft(3)}';
+
+ print("$header: ${code.wrap('Sample', forScript: forScript)}");
+ }
+}
diff --git a/pkgs/io/example/spawn_process_example.dart b/pkgs/io/example/spawn_process_example.dart
new file mode 100644
index 0000000..b7ba247
--- /dev/null
+++ b/pkgs/io/example/spawn_process_example.dart
@@ -0,0 +1,33 @@
+// Copyright (c) 2017, 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:async';
+
+import 'package:io/io.dart';
+
+/// Runs a few subcommands in the `dart` command.
+Future<void> main() async {
+ final manager = ProcessManager();
+
+ // Print `dart` tool version to stdout.
+ print('** Running `dart --version`');
+ var spawn = await manager.spawn('dart', ['--version']);
+ await spawn.exitCode;
+
+ // Check formatting and print the result to stdout.
+ print('** Running `dart format --output=none .`');
+ spawn = await manager.spawn('dart', ['format', '--output=none', '.']);
+ await spawn.exitCode;
+
+ // Check if a package is ready for publishing.
+ // Upon hitting a blocking stdin state, you may directly
+ // output to the processes's stdin via your own, similar to how a bash or
+ // shell script would spawn a process.
+ print('** Running pub publish');
+ spawn = await manager.spawn('dart', ['pub', 'publish', '--dry-run']);
+ await spawn.exitCode;
+
+ // Closes stdin for the entire program.
+ await sharedStdIn.terminate();
+}
diff --git a/pkgs/io/lib/ansi.dart b/pkgs/io/lib/ansi.dart
new file mode 100644
index 0000000..a2adbe7
--- /dev/null
+++ b/pkgs/io/lib/ansi.dart
@@ -0,0 +1,5 @@
+// Copyright (c) 2017, 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.
+
+export 'src/ansi_code.dart';
diff --git a/pkgs/io/lib/io.dart b/pkgs/io/lib/io.dart
new file mode 100644
index 0000000..8ee0843
--- /dev/null
+++ b/pkgs/io/lib/io.dart
@@ -0,0 +1,10 @@
+// Copyright (c) 2017, 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.
+
+export 'src/copy_path.dart' show copyPath, copyPathSync;
+export 'src/exit_code.dart' show ExitCode;
+export 'src/permissions.dart' show isExecutable;
+export 'src/process_manager.dart' show ProcessManager, Spawn, StartProcess;
+export 'src/shared_stdin.dart' show SharedStdIn, sharedStdIn;
+export 'src/shell_words.dart' show shellSplit;
diff --git a/pkgs/io/lib/src/ansi_code.dart b/pkgs/io/lib/src/ansi_code.dart
new file mode 100644
index 0000000..c9a22c5
--- /dev/null
+++ b/pkgs/io/lib/src/ansi_code.dart
@@ -0,0 +1,316 @@
+// Copyright (c) 2017, 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:async';
+import 'dart:io' as io;
+
+const _ansiEscapeLiteral = '\x1B';
+const _ansiEscapeForScript = r'\033';
+
+/// Whether formatted ANSI output is enabled for [wrapWith] and [AnsiCode.wrap].
+///
+/// By default, returns `true` if both `stdout.supportsAnsiEscapes` and
+/// `stderr.supportsAnsiEscapes` from `dart:io` are `true`.
+///
+/// The default can be overridden by setting the [Zone] variable [AnsiCode] to
+/// either `true` or `false`.
+///
+/// [overrideAnsiOutput] is provided to make this easy.
+bool get ansiOutputEnabled =>
+ Zone.current[AnsiCode] as bool? ??
+ (io.stdout.supportsAnsiEscapes && io.stderr.supportsAnsiEscapes);
+
+/// Returns `true` no formatting is required for [input].
+bool _isNoop(bool skip, String? input, bool? forScript) =>
+ skip ||
+ input == null ||
+ input.isEmpty ||
+ !((forScript ?? false) || ansiOutputEnabled);
+
+/// Allows overriding [ansiOutputEnabled] to [enableAnsiOutput] for the code run
+/// within [body].
+T overrideAnsiOutput<T>(bool enableAnsiOutput, T Function() body) =>
+ runZoned(body, zoneValues: <Object, Object>{AnsiCode: enableAnsiOutput});
+
+/// The type of code represented by [AnsiCode].
+class AnsiCodeType {
+ final String _name;
+
+ /// A foreground color.
+ static const AnsiCodeType foreground = AnsiCodeType._('foreground');
+
+ /// A style.
+ static const AnsiCodeType style = AnsiCodeType._('style');
+
+ /// A background color.
+ static const AnsiCodeType background = AnsiCodeType._('background');
+
+ /// A reset value.
+ static const AnsiCodeType reset = AnsiCodeType._('reset');
+
+ const AnsiCodeType._(this._name);
+
+ @override
+ String toString() => 'AnsiType.$_name';
+}
+
+/// Standard ANSI escape code for customizing terminal text output.
+///
+/// [Source](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors)
+class AnsiCode {
+ /// The numeric value associated with this code.
+ final int code;
+
+ /// The [AnsiCode] that resets this value, if one exists.
+ ///
+ /// Otherwise, `null`.
+ final AnsiCode? reset;
+
+ /// A description of this code.
+ final String name;
+
+ /// The type of code that is represented.
+ final AnsiCodeType type;
+
+ const AnsiCode._(this.name, this.type, this.code, this.reset);
+
+ /// Represents the value escaped for use in terminal output.
+ 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 ? 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. The
+ /// value of [ansiOutputEnabled] is also ignored.
+ ///
+ /// Returns `value` unchanged if
+ /// * [value] is `null` or empty
+ /// * both [ansiOutputEnabled] and [forScript] are `false`.
+ /// * [type] is [AnsiCodeType.reset]
+ String? wrap(String? value, {bool forScript = false}) =>
+ _isNoop(type == AnsiCodeType.reset, value, forScript)
+ ? value
+ : '${_escapeValue(forScript: forScript)}$value'
+ '${reset!._escapeValue(forScript: forScript)}';
+
+ @override
+ String toString() => '$name ${type._name} ($code)';
+}
+
+/// Returns a [String] formatted with [codes].
+///
+/// If [forScript] is `true`, the return value is an unescaped literal. The
+/// value of [ansiOutputEnabled] is also ignored.
+///
+/// Returns `value` unchanged if
+/// * [value] is `null` or empty.
+/// * both [ansiOutputEnabled] and [forScript] are `false`.
+/// * [codes] is empty.
+///
+/// Throws an [ArgumentError] if
+/// * [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,
+ {bool forScript = false}) {
+ // Eliminate duplicates
+ final myCodes = codes.toSet();
+
+ if (_isNoop(myCodes.isEmpty, value, forScript)) {
+ return value;
+ }
+
+ var foreground = 0, background = 0;
+ for (var code in myCodes) {
+ switch (code.type) {
+ case AnsiCodeType.foreground:
+ foreground++;
+ if (foreground > 1) {
+ throw ArgumentError.value(codes, 'codes',
+ 'Cannot contain more than one foreground color code.');
+ }
+ case AnsiCodeType.background:
+ background++;
+ if (background > 1) {
+ throw ArgumentError.value(codes, 'codes',
+ 'Cannot contain more than one foreground color code.');
+ }
+ case AnsiCodeType.reset:
+ throw ArgumentError.value(
+ codes, 'codes', 'Cannot contain reset codes.');
+ case AnsiCodeType.style:
+ // Ignore.
+ break;
+ }
+ }
+
+ final sortedCodes = myCodes.map((ac) => ac.code).toList()..sort();
+ final escapeValue = forScript ? _ansiEscapeForScript : _ansiEscapeLiteral;
+
+ return "$escapeValue[${sortedCodes.join(';')}m$value"
+ '${resetAll._escapeValue(forScript: forScript)}';
+}
+
+//
+// Style values
+//
+
+const styleBold = AnsiCode._('bold', AnsiCodeType.style, 1, resetBold);
+const styleDim = AnsiCode._('dim', AnsiCodeType.style, 2, resetDim);
+const styleItalic = AnsiCode._('italic', AnsiCodeType.style, 3, resetItalic);
+const styleUnderlined =
+ AnsiCode._('underlined', AnsiCodeType.style, 4, resetUnderlined);
+const styleBlink = AnsiCode._('blink', AnsiCodeType.style, 5, resetBlink);
+const styleReverse = AnsiCode._('reverse', AnsiCodeType.style, 7, resetReverse);
+
+/// Not widely supported.
+const styleHidden = AnsiCode._('hidden', AnsiCodeType.style, 8, resetHidden);
+
+/// Not widely supported.
+const styleCrossedOut =
+ AnsiCode._('crossed out', AnsiCodeType.style, 9, resetCrossedOut);
+
+//
+// Reset values
+//
+
+const resetAll = AnsiCode._('all', AnsiCodeType.reset, 0, null);
+
+// NOTE: bold is weird. The reset code seems to be 22 sometimes – not 21
+// See https://gitlab.com/gnachman/iterm2/issues/3208
+const resetBold = AnsiCode._('bold', AnsiCodeType.reset, 22, null);
+const resetDim = AnsiCode._('dim', AnsiCodeType.reset, 22, null);
+const resetItalic = AnsiCode._('italic', AnsiCodeType.reset, 23, null);
+const resetUnderlined = AnsiCode._('underlined', AnsiCodeType.reset, 24, null);
+const resetBlink = AnsiCode._('blink', AnsiCodeType.reset, 25, null);
+const resetReverse = AnsiCode._('reverse', AnsiCodeType.reset, 27, null);
+const resetHidden = AnsiCode._('hidden', AnsiCodeType.reset, 28, null);
+const resetCrossedOut = AnsiCode._('crossed out', AnsiCodeType.reset, 29, null);
+
+//
+// Foreground values
+//
+
+const black = AnsiCode._('black', AnsiCodeType.foreground, 30, resetAll);
+const red = AnsiCode._('red', AnsiCodeType.foreground, 31, resetAll);
+const green = AnsiCode._('green', AnsiCodeType.foreground, 32, resetAll);
+const yellow = AnsiCode._('yellow', AnsiCodeType.foreground, 33, resetAll);
+const blue = AnsiCode._('blue', AnsiCodeType.foreground, 34, resetAll);
+const magenta = AnsiCode._('magenta', AnsiCodeType.foreground, 35, resetAll);
+const cyan = AnsiCode._('cyan', AnsiCodeType.foreground, 36, resetAll);
+const lightGray =
+ AnsiCode._('light gray', AnsiCodeType.foreground, 37, resetAll);
+const defaultForeground =
+ AnsiCode._('default', AnsiCodeType.foreground, 39, resetAll);
+const darkGray = AnsiCode._('dark gray', AnsiCodeType.foreground, 90, resetAll);
+const lightRed = AnsiCode._('light red', AnsiCodeType.foreground, 91, resetAll);
+const lightGreen =
+ AnsiCode._('light green', AnsiCodeType.foreground, 92, resetAll);
+const lightYellow =
+ AnsiCode._('light yellow', AnsiCodeType.foreground, 93, resetAll);
+const lightBlue =
+ AnsiCode._('light blue', AnsiCodeType.foreground, 94, resetAll);
+const lightMagenta =
+ AnsiCode._('light magenta', AnsiCodeType.foreground, 95, resetAll);
+const lightCyan =
+ AnsiCode._('light cyan', AnsiCodeType.foreground, 96, resetAll);
+const white = AnsiCode._('white', AnsiCodeType.foreground, 97, resetAll);
+
+//
+// Background values
+//
+
+const backgroundBlack =
+ AnsiCode._('black', AnsiCodeType.background, 40, resetAll);
+const backgroundRed = AnsiCode._('red', AnsiCodeType.background, 41, resetAll);
+const backgroundGreen =
+ AnsiCode._('green', AnsiCodeType.background, 42, resetAll);
+const backgroundYellow =
+ AnsiCode._('yellow', AnsiCodeType.background, 43, resetAll);
+const backgroundBlue =
+ AnsiCode._('blue', AnsiCodeType.background, 44, resetAll);
+const backgroundMagenta =
+ AnsiCode._('magenta', AnsiCodeType.background, 45, resetAll);
+const backgroundCyan =
+ AnsiCode._('cyan', AnsiCodeType.background, 46, resetAll);
+const backgroundLightGray =
+ AnsiCode._('light gray', AnsiCodeType.background, 47, resetAll);
+const backgroundDefault =
+ AnsiCode._('default', AnsiCodeType.background, 49, resetAll);
+const backgroundDarkGray =
+ AnsiCode._('dark gray', AnsiCodeType.background, 100, resetAll);
+const backgroundLightRed =
+ AnsiCode._('light red', AnsiCodeType.background, 101, resetAll);
+const backgroundLightGreen =
+ AnsiCode._('light green', AnsiCodeType.background, 102, resetAll);
+const backgroundLightYellow =
+ AnsiCode._('light yellow', AnsiCodeType.background, 103, resetAll);
+const backgroundLightBlue =
+ AnsiCode._('light blue', AnsiCodeType.background, 104, resetAll);
+const backgroundLightMagenta =
+ AnsiCode._('light magenta', AnsiCodeType.background, 105, resetAll);
+const backgroundLightCyan =
+ AnsiCode._('light cyan', AnsiCodeType.background, 106, resetAll);
+const backgroundWhite =
+ AnsiCode._('white', AnsiCodeType.background, 107, resetAll);
+
+/// All of the [AnsiCode] values that represent [AnsiCodeType.style].
+const List<AnsiCode> styles = [
+ styleBold,
+ styleDim,
+ styleItalic,
+ styleUnderlined,
+ styleBlink,
+ styleReverse,
+ styleHidden,
+ styleCrossedOut
+];
+
+/// All of the [AnsiCode] values that represent [AnsiCodeType.foreground].
+const List<AnsiCode> foregroundColors = [
+ black,
+ red,
+ green,
+ yellow,
+ blue,
+ magenta,
+ cyan,
+ lightGray,
+ defaultForeground,
+ darkGray,
+ lightRed,
+ lightGreen,
+ lightYellow,
+ lightBlue,
+ lightMagenta,
+ lightCyan,
+ white
+];
+
+/// All of the [AnsiCode] values that represent [AnsiCodeType.background].
+const List<AnsiCode> backgroundColors = [
+ backgroundBlack,
+ backgroundRed,
+ backgroundGreen,
+ backgroundYellow,
+ backgroundBlue,
+ backgroundMagenta,
+ backgroundCyan,
+ backgroundLightGray,
+ backgroundDefault,
+ backgroundDarkGray,
+ backgroundLightRed,
+ backgroundLightGreen,
+ backgroundLightYellow,
+ backgroundLightBlue,
+ backgroundLightMagenta,
+ backgroundLightCyan,
+ backgroundWhite
+];
diff --git a/pkgs/io/lib/src/charcodes.dart b/pkgs/io/lib/src/charcodes.dart
new file mode 100644
index 0000000..4acaf0a
--- /dev/null
+++ b/pkgs/io/lib/src/charcodes.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2021, 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.
+
+// Generated using:
+// pub global run charcode \$=dollar \'=single_quote \"=double_quote \
+// \' '\\\n"$`# \t'
+
+/// "Horizontal Tab" control character, common name.
+const int $tab = 0x09;
+
+/// "Line feed" control character.
+const int $lf = 0x0a;
+
+/// Space character.
+const int $space = 0x20;
+
+/// Character `"`, short name.
+const int $doubleQuote = 0x22;
+
+/// Character `#`.
+const int $hash = 0x23;
+
+/// Character `$`.
+const int $dollar = 0x24;
+
+/// Character "'".
+const int $singleQuote = 0x27;
+
+/// Character `\`.
+const int $backslash = 0x5c;
+
+/// Character `` ` ``.
+const int $backquote = 0x60;
diff --git a/pkgs/io/lib/src/copy_path.dart b/pkgs/io/lib/src/copy_path.dart
new file mode 100644
index 0000000..3a999b6
--- /dev/null
+++ b/pkgs/io/lib/src/copy_path.dart
@@ -0,0 +1,69 @@
+// Copyright (c) 2017, 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:io';
+
+import 'package:path/path.dart' as p;
+
+bool _doNothing(String from, String to) {
+ if (p.canonicalize(from) == p.canonicalize(to)) {
+ return true;
+ }
+ if (p.isWithin(from, to)) {
+ throw ArgumentError('Cannot copy from $from to $to');
+ }
+ return false;
+}
+
+/// Copies all of the files in the [from] directory to [to].
+///
+/// This is similar to `cp -R <from> <to>`:
+/// * Symlinks are supported.
+/// * Existing files are over-written, if any.
+/// * If [to] is within [from], throws [ArgumentError] (an infinite operation).
+/// * If [from] and [to] are canonically the same, no operation occurs.
+///
+/// Returns a future that completes when complete.
+Future<void> copyPath(String from, String to) async {
+ if (_doNothing(from, to)) {
+ return;
+ }
+ await Directory(to).create(recursive: true);
+ await for (final file in Directory(from).list(recursive: true)) {
+ final copyTo = p.join(to, p.relative(file.path, from: from));
+ if (file is Directory) {
+ await Directory(copyTo).create(recursive: true);
+ } else if (file is File) {
+ await File(file.path).copy(copyTo);
+ } else if (file is Link) {
+ await Link(copyTo).create(await file.target(), recursive: true);
+ }
+ }
+}
+
+/// Copies all of the files in the [from] directory to [to].
+///
+/// This is similar to `cp -R <from> <to>`:
+/// * Symlinks are supported.
+/// * Existing files are over-written, if any.
+/// * If [to] is within [from], throws [ArgumentError] (an infinite operation).
+/// * If [from] and [to] are canonically the same, no operation occurs.
+///
+/// This action is performed synchronously (blocking I/O).
+void copyPathSync(String from, String to) {
+ if (_doNothing(from, to)) {
+ return;
+ }
+ Directory(to).createSync(recursive: true);
+ for (final file in Directory(from).listSync(recursive: true)) {
+ final copyTo = p.join(to, p.relative(file.path, from: from));
+ if (file is Directory) {
+ Directory(copyTo).createSync(recursive: true);
+ } else if (file is File) {
+ File(file.path).copySync(copyTo);
+ } else if (file is Link) {
+ Link(copyTo).createSync(file.targetSync(), recursive: true);
+ }
+ }
+}
diff --git a/pkgs/io/lib/src/exit_code.dart b/pkgs/io/lib/src/exit_code.dart
new file mode 100644
index 0000000..d405558
--- /dev/null
+++ b/pkgs/io/lib/src/exit_code.dart
@@ -0,0 +1,82 @@
+// Copyright (c) 2017, 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.
+
+/// Exit code constants.
+///
+/// [Source](https://www.freebsd.org/cgi/man.cgi?query=sysexits).
+class ExitCode {
+ /// Command completed successfully.
+ static const success = ExitCode._(0, 'success');
+
+ /// Command was used incorrectly.
+ ///
+ /// This may occur if the wrong number of arguments was used, a bad flag, or
+ /// bad syntax in a parameter.
+ static const usage = ExitCode._(64, 'usage');
+
+ /// Input data was used incorrectly.
+ ///
+ /// This should occur only for user data (not system files).
+ static const data = ExitCode._(65, 'data');
+
+ /// An input file (not a system file) did not exist or was not readable.
+ static const noInput = ExitCode._(66, 'noInput');
+
+ /// User specified did not exist.
+ static const noUser = ExitCode._(67, 'noUser');
+
+ /// Host specified did not exist.
+ static const noHost = ExitCode._(68, 'noHost');
+
+ /// A service is unavailable.
+ ///
+ /// This may occur if a support program or file does not exist. This may also
+ /// be used as a catch-all error when something you wanted to do does not
+ /// work, but you do not know why.
+ static const unavailable = ExitCode._(69, 'unavailable');
+
+ /// An internal software error has been detected.
+ ///
+ /// This should be limited to non-operating system related errors as possible.
+ static const software = ExitCode._(70, 'software');
+
+ /// An operating system error has been detected.
+ ///
+ /// This intended to be used for such thing as `cannot fork` or `cannot pipe`.
+ static const osError = ExitCode._(71, 'osError');
+
+ /// Some system file (e.g. `/etc/passwd`) does not exist or could not be read.
+ static const osFile = ExitCode._(72, 'osFile');
+
+ /// A (user specified) output file cannot be created.
+ static const cantCreate = ExitCode._(73, 'cantCreate');
+
+ /// An error occurred doing I/O on some file.
+ static const ioError = ExitCode._(74, 'ioError');
+
+ /// Temporary failure, indicating something is not really an error.
+ ///
+ /// In some cases, this can be re-attempted and will succeed later.
+ static const tempFail = ExitCode._(75, 'tempFail');
+
+ /// You did not have sufficient permissions to perform the operation.
+ ///
+ /// This is not intended for file system problems, which should use [noInput]
+ /// or [cantCreate], but rather for higher-level permissions.
+ static const noPerm = ExitCode._(77, 'noPerm');
+
+ /// Something was found in an unconfigured or misconfigured state.
+ static const config = ExitCode._(78, 'config');
+
+ /// Exit code value.
+ final int code;
+
+ /// Name of the exit code.
+ final String _name;
+
+ const ExitCode._(this.code, this._name);
+
+ @override
+ String toString() => '$_name: $code';
+}
diff --git a/pkgs/io/lib/src/permissions.dart b/pkgs/io/lib/src/permissions.dart
new file mode 100644
index 0000000..c516943
--- /dev/null
+++ b/pkgs/io/lib/src/permissions.dart
@@ -0,0 +1,69 @@
+// Copyright (c) 2017, 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:async';
+import 'dart:io';
+
+/// What type of permission is granted to a file based on file permission roles.
+enum _FilePermission {
+ execute,
+ // Although these two values are unused, their positions in the enum are
+ // meaningful.
+ write, // ignore: unused_field
+ read, // ignore: unused_field
+ setGid,
+ setUid,
+ sticky,
+}
+
+/// What type of role is assigned to a file.
+enum _FilePermissionRole {
+ world,
+ group,
+ user,
+}
+
+/// Returns whether file [stat] has [permission] for a [role] type.
+bool _hasPermission(
+ FileStat stat,
+ _FilePermission permission, {
+ _FilePermissionRole role = _FilePermissionRole.world,
+}) {
+ final index = _permissionBitIndex(permission, role);
+ return (stat.mode & (1 << index)) != 0;
+}
+
+int _permissionBitIndex(_FilePermission permission, _FilePermissionRole role) =>
+ switch (permission) {
+ _FilePermission.setUid => 11,
+ _FilePermission.setGid => 10,
+ _FilePermission.sticky => 9,
+ _ => (role.index * 3) + permission.index
+ };
+
+/// Returns whether [path] is considered an executable file on this OS.
+///
+/// May optionally define how to implement [getStat] or whether to execute based
+/// on whether this is the windows platform ([isWindows]) - if not set it is
+/// automatically extracted from `dart:io#Platform`.
+///
+/// **NOTE**: On windows this always returns `true`.
+FutureOr<bool> isExecutable(
+ String path, {
+ bool? isWindows,
+ FutureOr<FileStat> Function(String path) getStat = FileStat.stat,
+}) {
+ // Windows has no concept of executable.
+ if (isWindows ?? Platform.isWindows) return true;
+ final stat = getStat(path);
+ if (stat is FileStat) {
+ return _isExecutable(stat);
+ }
+ return stat.then(_isExecutable);
+}
+
+bool _isExecutable(FileStat stat) =>
+ stat.type == FileSystemEntityType.file &&
+ _FilePermissionRole.values.any(
+ (role) => _hasPermission(stat, _FilePermission.execute, role: role));
diff --git a/pkgs/io/lib/src/process_manager.dart b/pkgs/io/lib/src/process_manager.dart
new file mode 100644
index 0000000..84d22ec
--- /dev/null
+++ b/pkgs/io/lib/src/process_manager.dart
@@ -0,0 +1,255 @@
+// Copyright (c) 2017, 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.
+
+// ignore_for_file: close_sinks,cancel_subscriptions
+
+import 'dart:async';
+import 'dart:io' as io;
+
+import 'package:meta/meta.dart';
+
+import 'shared_stdin.dart';
+
+/// Type definition for both [io.Process.start] and [ProcessManager.spawn].
+///
+/// Useful for taking different implementations of this base functionality.
+typedef StartProcess = Future<io.Process> Function(
+ String executable,
+ List<String> arguments, {
+ String workingDirectory,
+ Map<String, String> environment,
+ bool includeParentEnvironment,
+ bool runInShell,
+ io.ProcessStartMode mode,
+});
+
+/// A high-level abstraction around using and managing processes on the system.
+abstract class ProcessManager {
+ /// Terminates the global `stdin` listener, making future listens impossible.
+ ///
+ /// This method should be invoked only at the _end_ of a program's execution.
+ static Future<void> terminateStdIn() async {
+ await sharedStdIn.terminate();
+ }
+
+ /// Create a new instance of [ProcessManager] for the current platform.
+ ///
+ /// May manually specify whether the current platform [isWindows], otherwise
+ /// this is derived from the Dart runtime (i.e. [io.Platform.isWindows]).
+ factory ProcessManager({
+ Stream<List<int>>? stdin,
+ io.IOSink? stdout,
+ io.IOSink? stderr,
+ bool? isWindows,
+ }) {
+ stdin ??= sharedStdIn;
+ stdout ??= io.stdout;
+ stderr ??= io.stderr;
+ isWindows ??= io.Platform.isWindows;
+ if (isWindows) {
+ return _WindowsProcessManager(stdin, stdout, stderr);
+ }
+ return _UnixProcessManager(stdin, stdout, stderr);
+ }
+
+ final Stream<List<int>> _stdin;
+ final io.IOSink _stdout;
+ final io.IOSink _stderr;
+
+ const ProcessManager._(this._stdin, this._stdout, this._stderr);
+
+ /// Spawns a process by invoking [executable] with [arguments].
+ ///
+ /// This is _similar_ to [io.Process.start], but all standard input and output
+ /// is forwarded/routed between the process and the host, similar to how a
+ /// shell script works.
+ ///
+ /// Returns a future that completes with a handle to the spawned process.
+ Future<io.Process> spawn(
+ String executable,
+ Iterable<String> arguments, {
+ String? workingDirectory,
+ Map<String, String>? environment,
+ bool includeParentEnvironment = true,
+ bool runInShell = false,
+ io.ProcessStartMode mode = io.ProcessStartMode.normal,
+ }) async {
+ final process = io.Process.start(
+ executable,
+ arguments.toList(),
+ workingDirectory: workingDirectory,
+ environment: environment,
+ includeParentEnvironment: includeParentEnvironment,
+ runInShell: runInShell,
+ mode: mode,
+ );
+ return _ForwardingSpawn(await process, _stdin, _stdout, _stderr);
+ }
+
+ /// Spawns a process by invoking [executable] with [arguments].
+ ///
+ /// This is _similar_ to [io.Process.start], but `stdout` and `stderr` is
+ /// forwarded/routed between the process and host, similar to how a shell
+ /// script works.
+ ///
+ /// Returns a future that completes with a handle to the spawned process.
+ Future<io.Process> spawnBackground(
+ String executable,
+ Iterable<String> arguments, {
+ String? workingDirectory,
+ Map<String, String>? environment,
+ bool includeParentEnvironment = true,
+ bool runInShell = false,
+ io.ProcessStartMode mode = io.ProcessStartMode.normal,
+ }) async {
+ final process = io.Process.start(
+ executable,
+ arguments.toList(),
+ workingDirectory: workingDirectory,
+ environment: environment,
+ includeParentEnvironment: includeParentEnvironment,
+ runInShell: runInShell,
+ mode: mode,
+ );
+ return _ForwardingSpawn(
+ await process,
+ const Stream.empty(),
+ _stdout,
+ _stderr,
+ );
+ }
+
+ /// Spawns a process by invoking [executable] with [arguments].
+ ///
+ /// This is _identical to [io.Process.start] (no forwarding of I/O).
+ ///
+ /// Returns a future that completes with a handle to the spawned process.
+ Future<io.Process> spawnDetached(
+ String executable,
+ Iterable<String> arguments, {
+ String? workingDirectory,
+ Map<String, String>? environment,
+ bool includeParentEnvironment = true,
+ bool runInShell = false,
+ io.ProcessStartMode mode = io.ProcessStartMode.normal,
+ }) async =>
+ io.Process.start(
+ executable,
+ arguments.toList(),
+ workingDirectory: workingDirectory,
+ environment: environment,
+ includeParentEnvironment: includeParentEnvironment,
+ runInShell: runInShell,
+ mode: mode,
+ );
+}
+
+/// A process instance created and managed through [ProcessManager].
+///
+/// Unlike one created directly by [io.Process.start] or [io.Process.run], a
+/// spawned process works more like executing a command in a shell script.
+class Spawn implements io.Process {
+ final io.Process _delegate;
+
+ Spawn._(this._delegate) {
+ _delegate.exitCode.then((_) => _onClosed());
+ }
+
+ @mustCallSuper
+ void _onClosed() {}
+
+ @override
+ bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) =>
+ _delegate.kill(signal);
+
+ @override
+ Future<int> get exitCode => _delegate.exitCode;
+
+ @override
+ int get pid => _delegate.pid;
+
+ @override
+ Stream<List<int>> get stderr => _delegate.stderr;
+
+ @override
+ io.IOSink get stdin => _delegate.stdin;
+
+ @override
+ Stream<List<int>> get stdout => _delegate.stdout;
+}
+
+/// Forwards `stdin`/`stdout`/`stderr` to/from the host.
+class _ForwardingSpawn extends Spawn {
+ final StreamSubscription<List<int>> _stdInSub;
+ final StreamSubscription<List<int>> _stdOutSub;
+ final StreamSubscription<List<int>> _stdErrSub;
+ final StreamController<List<int>> _stdOut;
+ final StreamController<List<int>> _stdErr;
+
+ factory _ForwardingSpawn(
+ io.Process delegate,
+ Stream<List<int>> stdin,
+ io.IOSink stdout,
+ io.IOSink stderr,
+ ) {
+ final stdoutSelf = StreamController<List<int>>();
+ final stderrSelf = StreamController<List<int>>();
+ final stdInSub = stdin.listen(delegate.stdin.add);
+ final stdOutSub = delegate.stdout.listen((event) {
+ stdout.add(event);
+ stdoutSelf.add(event);
+ });
+ final stdErrSub = delegate.stderr.listen((event) {
+ stderr.add(event);
+ stderrSelf.add(event);
+ });
+ return _ForwardingSpawn._delegate(
+ delegate,
+ stdInSub,
+ stdOutSub,
+ stdErrSub,
+ stdoutSelf,
+ stderrSelf,
+ );
+ }
+
+ _ForwardingSpawn._delegate(
+ super.delegate,
+ this._stdInSub,
+ this._stdOutSub,
+ this._stdErrSub,
+ this._stdOut,
+ this._stdErr,
+ ) : super._();
+
+ @override
+ void _onClosed() {
+ _stdInSub.cancel();
+ _stdOutSub.cancel();
+ _stdErrSub.cancel();
+ super._onClosed();
+ }
+
+ @override
+ Stream<List<int>> get stdout => _stdOut.stream;
+
+ @override
+ Stream<List<int>> get stderr => _stdErr.stream;
+}
+
+class _UnixProcessManager extends ProcessManager {
+ const _UnixProcessManager(
+ super.stdin,
+ super.stdout,
+ super.stderr,
+ ) : super._();
+}
+
+class _WindowsProcessManager extends ProcessManager {
+ const _WindowsProcessManager(
+ super.stdin,
+ super.stdout,
+ super.stderr,
+ ) : super._();
+}
diff --git a/pkgs/io/lib/src/shared_stdin.dart b/pkgs/io/lib/src/shared_stdin.dart
new file mode 100644
index 0000000..72bb50c
--- /dev/null
+++ b/pkgs/io/lib/src/shared_stdin.dart
@@ -0,0 +1,99 @@
+// Copyright (c) 2017, 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:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:meta/meta.dart';
+
+/// A shared singleton instance of `dart:io`'s [stdin] stream.
+///
+/// _Unlike_ the normal [stdin] stream, [sharedStdIn] may switch subscribers
+/// as long as the previous subscriber cancels before the new subscriber starts
+/// listening.
+///
+/// [SharedStdIn.terminate] *must* be invoked in order to close the underlying
+/// connection to [stdin], allowing your program to close automatically without
+/// hanging.
+final SharedStdIn sharedStdIn = SharedStdIn(stdin);
+
+/// A singleton wrapper around `stdin` that allows new subscribers.
+///
+/// This class is visible in order to be used as a test harness for mock
+/// implementations of `stdin`. In normal programs, [sharedStdIn] should be
+/// used directly.
+@visibleForTesting
+class SharedStdIn extends Stream<List<int>> {
+ StreamController<List<int>>? _current;
+ StreamSubscription<List<int>>? _sub;
+
+ SharedStdIn([Stream<List<int>>? stream]) {
+ _sub = (stream ??= stdin).listen(_onInput);
+ }
+
+ /// Returns a future that completes with the next line.
+ ///
+ /// This is similar to the standard [Stdin.readLineSync], but asynchronous.
+ Future<String> nextLine({Encoding encoding = systemEncoding}) =>
+ lines(encoding: encoding).first;
+
+ /// Returns the stream transformed as UTF8 strings separated by line breaks.
+ ///
+ /// This is similar to synchronous code using [Stdin.readLineSync]:
+ /// ```dart
+ /// while (true) {
+ /// var line = stdin.readLineSync();
+ /// // ...
+ /// }
+ /// ```
+ ///
+ /// ... but asynchronous.
+ Stream<String> lines({Encoding encoding = systemEncoding}) =>
+ transform(utf8.decoder).transform(const LineSplitter());
+
+ void _onInput(List<int> event) => _getCurrent().add(event);
+
+ StreamController<List<int>> _getCurrent() =>
+ _current ??= StreamController<List<int>>(
+ onCancel: () {
+ _current = null;
+ },
+ sync: true);
+
+ @override
+ StreamSubscription<List<int>> listen(
+ void Function(List<int> event)? onData, {
+ Function? onError,
+ void Function()? onDone,
+ bool? cancelOnError,
+ }) {
+ if (_sub == null) {
+ throw StateError('Stdin has already been terminated.');
+ }
+ // ignore: close_sinks
+ final controller = _getCurrent();
+ if (controller.hasListener) {
+ throw StateError(''
+ 'Subscriber already listening. The existing subscriber must cancel '
+ 'before another may be added.');
+ }
+ return controller.stream.listen(
+ onData,
+ onDone: onDone,
+ onError: onError,
+ cancelOnError: cancelOnError,
+ );
+ }
+
+ /// Terminates the connection to `stdin`, closing all subscription.
+ Future<void> terminate() async {
+ if (_sub == null) {
+ throw StateError('Stdin has already been terminated.');
+ }
+ await _sub?.cancel();
+ await _current?.close();
+ _sub = null;
+ }
+}
diff --git a/pkgs/io/lib/src/shell_words.dart b/pkgs/io/lib/src/shell_words.dart
new file mode 100644
index 0000000..5fca6d9
--- /dev/null
+++ b/pkgs/io/lib/src/shell_words.dart
@@ -0,0 +1,142 @@
+// Copyright (c) 2017, 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.
+
+// ignore_for_file: comment_references
+
+import 'package:string_scanner/string_scanner.dart';
+
+import 'charcodes.dart';
+
+/// Splits [command] into tokens according to [the POSIX shell
+/// specification][spec].
+///
+/// [spec]: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/contents.html
+///
+/// This returns the unquoted values of quoted tokens. For example,
+/// `shellSplit('foo "bar baz"')` returns `["foo", "bar baz"]`. It does not
+/// currently support here-documents. It does *not* treat dynamic features such
+/// as parameter expansion specially. For example, `shellSplit("foo $(bar
+/// baz)")` returns `["foo", "$(bar", "baz)"]`.
+///
+/// This will discard any comments at the end of [command].
+///
+/// Throws a [FormatException] if [command] isn't a valid shell command.
+List<String> shellSplit(String command) {
+ final scanner = StringScanner(command);
+ final results = <String>[];
+ final token = StringBuffer();
+
+ // Whether a token is being parsed, as opposed to a separator character. This
+ // is different than just [token.isEmpty], because empty quoted tokens can
+ // exist.
+ var hasToken = false;
+
+ while (!scanner.isDone) {
+ final next = scanner.readChar();
+ switch (next) {
+ case $backslash:
+ // Section 2.2.1: A <backslash> that is not quoted shall preserve the
+ // literal value of the following character, with the exception of a
+ // <newline>. If a <newline> follows the <backslash>, the shell shall
+ // interpret this as line continuation. The <backslash> and <newline>
+ // shall be removed before splitting the input into tokens. Since the
+ // escaped <newline> is removed entirely from the input and is not
+ // replaced by any white space, it cannot serve as a token separator.
+ if (scanner.scanChar($lf)) break;
+
+ hasToken = true;
+ token.writeCharCode(scanner.readChar());
+
+ case $singleQuote:
+ hasToken = true;
+ // Section 2.2.2: Enclosing characters in single-quotes ( '' ) shall
+ // preserve the literal value of each character within the
+ // single-quotes. A single-quote cannot occur within single-quotes.
+ final firstQuote = scanner.position - 1;
+ while (!scanner.scanChar($singleQuote)) {
+ _checkUnmatchedQuote(scanner, firstQuote);
+ token.writeCharCode(scanner.readChar());
+ }
+
+ case $doubleQuote:
+ hasToken = true;
+ // Section 2.2.3: Enclosing characters in double-quotes ( "" ) shall
+ // preserve the literal value of all characters within the
+ // double-quotes, with the exception of the characters backquote,
+ // <dollar-sign>, and <backslash>.
+ //
+ // (Note that this code doesn't preserve special behavior of backquote
+ // or dollar sign within double quotes, since those are dynamic
+ // features.)
+ final firstQuote = scanner.position - 1;
+ while (!scanner.scanChar($doubleQuote)) {
+ _checkUnmatchedQuote(scanner, firstQuote);
+
+ if (scanner.scanChar($backslash)) {
+ _checkUnmatchedQuote(scanner, firstQuote);
+
+ // The <backslash> shall retain its special meaning as an escape
+ // character (see Escape Character (Backslash)) only when followed
+ // by one of the following characters when considered special:
+ //
+ // $ ` " \ <newline>
+ final next = scanner.readChar();
+ if (next == $lf) continue;
+ if (next == $dollar ||
+ next == $backquote ||
+ next == $doubleQuote ||
+ next == $backslash) {
+ token.writeCharCode(next);
+ } else {
+ token
+ ..writeCharCode($backslash)
+ ..writeCharCode(next);
+ }
+ } else {
+ token.writeCharCode(scanner.readChar());
+ }
+ }
+
+ case $hash:
+ // Section 2.3: If the current character is a '#' [and the previous
+ // characters was not part of a word], it and all subsequent characters
+ // up to, but excluding, the next <newline> shall be discarded as a
+ // comment. The <newline> that ends the line is not considered part of
+ // the comment.
+ if (hasToken) {
+ token.writeCharCode($hash);
+ break;
+ }
+
+ while (!scanner.isDone && scanner.peekChar() != $lf) {
+ scanner.readChar();
+ }
+
+ case $space:
+ case $tab:
+ case $lf:
+ // ignore: invariant_booleans
+ if (hasToken) results.add(token.toString());
+ hasToken = false;
+ token.clear();
+
+ default:
+ hasToken = true;
+ token.writeCharCode(next);
+ }
+ }
+
+ if (hasToken) results.add(token.toString());
+ return results;
+}
+
+/// Throws a [FormatException] if [scanner] is done indicating that a closing
+/// quote matching the one at position [openingQuote] is missing.
+void _checkUnmatchedQuote(StringScanner scanner, int openingQuote) {
+ if (!scanner.isDone) return;
+ final type = scanner.substring(openingQuote, openingQuote + 1) == '"'
+ ? 'double'
+ : 'single';
+ scanner.error('Unmatched $type quote.', position: openingQuote, length: 1);
+}
diff --git a/pkgs/io/pubspec.yaml b/pkgs/io/pubspec.yaml
new file mode 100644
index 0000000..7e00d99
--- /dev/null
+++ b/pkgs/io/pubspec.yaml
@@ -0,0 +1,19 @@
+name: io
+description: >-
+ Utilities for the Dart VM Runtime including support for ANSI colors, file
+ copying, and standard exit code values.
+version: 1.0.5
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/io
+
+environment:
+ sdk: ^3.4.0
+
+dependencies:
+ meta: ^1.3.0
+ path: ^1.8.0
+ string_scanner: ^1.1.0
+
+dev_dependencies:
+ dart_flutter_team_lints: ^3.0.0
+ test: ^1.16.6
+ test_descriptor: ^2.0.0
diff --git a/pkgs/io/test/_files/is_executable.sh b/pkgs/io/test/_files/is_executable.sh
new file mode 100755
index 0000000..f1f641a
--- /dev/null
+++ b/pkgs/io/test/_files/is_executable.sh
@@ -0,0 +1 @@
+#!/usr/bin/env bash
diff --git a/pkgs/io/test/_files/is_not_executable.sh b/pkgs/io/test/_files/is_not_executable.sh
new file mode 100644
index 0000000..f1f641a
--- /dev/null
+++ b/pkgs/io/test/_files/is_not_executable.sh
@@ -0,0 +1 @@
+#!/usr/bin/env bash
diff --git a/pkgs/io/test/_files/stderr_hello.dart b/pkgs/io/test/_files/stderr_hello.dart
new file mode 100644
index 0000000..ac7a7d3
--- /dev/null
+++ b/pkgs/io/test/_files/stderr_hello.dart
@@ -0,0 +1,7 @@
+// Copyright (c) 2017, 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:io';
+
+void main() => stderr.write('Hello');
diff --git a/pkgs/io/test/_files/stdin_echo.dart b/pkgs/io/test/_files/stdin_echo.dart
new file mode 100644
index 0000000..256e0ee
--- /dev/null
+++ b/pkgs/io/test/_files/stdin_echo.dart
@@ -0,0 +1,7 @@
+// Copyright (c) 2017, 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:io';
+
+void main() => stdout.writeln('You said: ${stdin.readLineSync()}');
diff --git a/pkgs/io/test/_files/stdout_hello.dart b/pkgs/io/test/_files/stdout_hello.dart
new file mode 100644
index 0000000..af3bf51
--- /dev/null
+++ b/pkgs/io/test/_files/stdout_hello.dart
@@ -0,0 +1,7 @@
+// Copyright (c) 2017, 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:io';
+
+void main() => stdout.write('Hello');
diff --git a/pkgs/io/test/ansi_code_test.dart b/pkgs/io/test/ansi_code_test.dart
new file mode 100644
index 0000000..98ae68b
--- /dev/null
+++ b/pkgs/io/test/ansi_code_test.dart
@@ -0,0 +1,187 @@
+// Copyright (c) 2017, 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:io';
+
+import 'package:io/ansi.dart';
+import 'package:test/test.dart';
+
+const _ansiEscapeLiteral = '\x1B';
+const _ansiEscapeForScript = r'\033';
+const sampleInput = 'sample input';
+
+void main() {
+ group('ansiOutputEnabled', () {
+ test('default value matches dart:io', () {
+ expect(ansiOutputEnabled,
+ stdout.supportsAnsiEscapes && stderr.supportsAnsiEscapes);
+ });
+
+ test('override true', () {
+ overrideAnsiOutput(true, () {
+ expect(ansiOutputEnabled, isTrue);
+ });
+ });
+
+ test('override false', () {
+ overrideAnsiOutput(false, () {
+ expect(ansiOutputEnabled, isFalse);
+ });
+ });
+
+ test('forScript variaents ignore `ansiOutputEnabled`', () {
+ const expected =
+ '$_ansiEscapeForScript[34m$sampleInput$_ansiEscapeForScript[0m';
+
+ for (var override in [true, false]) {
+ overrideAnsiOutput(override, () {
+ expect(blue.escapeForScript, '$_ansiEscapeForScript[34m');
+ expect(blue.wrap(sampleInput, forScript: true), expected);
+ expect(wrapWith(sampleInput, [blue], forScript: true), expected);
+ });
+ }
+ });
+ });
+
+ test('foreground and background colors match', () {
+ expect(foregroundColors, hasLength(backgroundColors.length));
+
+ for (var i = 0; i < foregroundColors.length; i++) {
+ final foreground = foregroundColors[i];
+ expect(foreground.type, AnsiCodeType.foreground);
+ expect(foreground.name.toLowerCase(), foreground.name,
+ reason: 'All names should be lower case');
+ final background = backgroundColors[i];
+ expect(background.type, AnsiCodeType.background);
+ expect(background.name.toLowerCase(), background.name,
+ reason: 'All names should be lower case');
+
+ expect(foreground.name, background.name);
+
+ // The last base-10 digit also matches – good to sanity check
+ expect(foreground.code % 10, background.code % 10);
+ }
+ });
+
+ test('all styles are styles', () {
+ for (var style in styles) {
+ expect(style.type, AnsiCodeType.style);
+ expect(style.name.toLowerCase(), style.name,
+ reason: 'All names should be lower case');
+ if (style == styleBold) {
+ expect(style.reset, resetBold);
+ } else {
+ expect(style.reset!.code, equals(style.code + 20));
+ }
+ expect(style.name, equals(style.reset!.name));
+ }
+ });
+
+ for (var forScript in [true, false]) {
+ group(forScript ? 'forScript' : 'escaped', () {
+ final escapeLiteral =
+ forScript ? _ansiEscapeForScript : _ansiEscapeLiteral;
+
+ 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);
+ });
+ });
+ });
+ }
+}
+
+void _test<T>(String name, T Function() body) =>
+ test(name, () => overrideAnsiOutput<T>(true, body));
diff --git a/pkgs/io/test/copy_path_test.dart b/pkgs/io/test/copy_path_test.dart
new file mode 100644
index 0000000..fd1e9ce
--- /dev/null
+++ b/pkgs/io/test/copy_path_test.dart
@@ -0,0 +1,45 @@
+// Copyright (c) 2017, 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.
+
+@TestOn('vm')
+library;
+
+import 'package:io/io.dart';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+void main() {
+ test('should copy a directory (async)', () async {
+ await _create();
+ await copyPath(p.join(d.sandbox, 'parent'), p.join(d.sandbox, 'copy'));
+ await _validate();
+ });
+
+ test('should copy a directory (sync)', () async {
+ await _create();
+ copyPathSync(p.join(d.sandbox, 'parent'), p.join(d.sandbox, 'copy'));
+ await _validate();
+ });
+
+ test('should catch an infinite operation', () async {
+ await _create();
+ expect(
+ copyPath(
+ p.join(d.sandbox, 'parent'),
+ p.join(d.sandbox, 'parent', 'child'),
+ ),
+ throwsArgumentError,
+ );
+ });
+}
+
+d.DirectoryDescriptor _struct() => d.dir('parent', [
+ d.dir('child', [
+ d.file('foo.txt'),
+ ]),
+ ]);
+
+Future<void> _create() => _struct().create();
+Future<void> _validate() => _struct().validate();
diff --git a/pkgs/io/test/permissions_test.dart b/pkgs/io/test/permissions_test.dart
new file mode 100644
index 0000000..478e8df
--- /dev/null
+++ b/pkgs/io/test/permissions_test.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2017, 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.
+
+@TestOn('vm')
+library;
+
+import 'package:io/io.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('isExecutable', () {
+ const files = 'test/_files';
+ const shellIsExec = '$files/is_executable.sh';
+ const shellNotExec = '$files/is_not_executable.sh';
+
+ group('on shell scripts', () {
+ test('should return true for "is_executable.sh"', () async {
+ expect(await isExecutable(shellIsExec), isTrue);
+ });
+
+ test('should return false for "is_not_executable.sh"', () async {
+ expect(await isExecutable(shellNotExec), isFalse);
+ });
+ }, testOn: '!windows');
+
+ group('on shell scripts [windows]', () {
+ test('should return true for "is_executable.sh"', () async {
+ expect(await isExecutable(shellIsExec, isWindows: true), isTrue);
+ });
+
+ test('should return true for "is_not_executable.sh"', () async {
+ expect(await isExecutable(shellNotExec, isWindows: true), isTrue);
+ });
+ });
+ });
+}
diff --git a/pkgs/io/test/process_manager_test.dart b/pkgs/io/test/process_manager_test.dart
new file mode 100644
index 0000000..9871a77
--- /dev/null
+++ b/pkgs/io/test/process_manager_test.dart
@@ -0,0 +1,100 @@
+// Copyright (c) 2017, 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.
+
+// ignore_for_file: close_sinks
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:io/io.dart' hide sharedStdIn;
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+void main() {
+ StreamController<String> fakeStdIn;
+ late ProcessManager processManager;
+ SharedStdIn sharedStdIn;
+ late List<String> stdoutLog;
+ late List<String> stderrLog;
+
+ test('spawn functions should match the type definition of Process.start', () {
+ const isStartProcess = TypeMatcher<StartProcess>();
+ expect(Process.start, isStartProcess);
+ final manager = ProcessManager();
+ expect(manager.spawn, isStartProcess);
+ expect(manager.spawnBackground, isStartProcess);
+ expect(manager.spawnDetached, isStartProcess);
+ });
+
+ group('spawn', () {
+ setUp(() async {
+ fakeStdIn = StreamController<String>(sync: true);
+ sharedStdIn = SharedStdIn(fakeStdIn.stream.map((s) => s.codeUnits));
+ stdoutLog = <String>[];
+ stderrLog = <String>[];
+
+ final stdoutController = StreamController<List<int>>(sync: true);
+ stdoutController.stream.map(utf8.decode).listen(stdoutLog.add);
+ final stdout = IOSink(stdoutController);
+ final stderrController = StreamController<List<int>>(sync: true);
+ stderrController.stream.map(utf8.decode).listen(stderrLog.add);
+ final stderr = IOSink(stderrController);
+
+ processManager = ProcessManager(
+ stdin: sharedStdIn,
+ stdout: stdout,
+ stderr: stderr,
+ );
+ });
+
+ final dart = Platform.executable;
+
+ test('should output Hello from another process [via stdout]', () async {
+ final spawn = await processManager.spawn(
+ dart,
+ [p.join('test', '_files', 'stdout_hello.dart')],
+ );
+ await spawn.exitCode;
+ expect(stdoutLog, ['Hello']);
+ });
+
+ test('should output Hello from another process [via stderr]', () async {
+ final spawn = await processManager.spawn(
+ dart,
+ [p.join('test', '_files', 'stderr_hello.dart')],
+ );
+ await spawn.exitCode;
+ expect(stderrLog, ['Hello']);
+ });
+
+ test('should forward stdin to another process', () async {
+ final spawn = await processManager.spawn(
+ dart,
+ [p.join('test', '_files', 'stdin_echo.dart')],
+ );
+ spawn.stdin.writeln('Ping');
+ await spawn.exitCode;
+ expect(stdoutLog.join(), contains('You said: Ping'));
+ });
+
+ group('should return a Process where', () {
+ test('.stdout is readable', () async {
+ final spawn = await processManager.spawn(
+ dart,
+ [p.join('test', '_files', 'stdout_hello.dart')],
+ );
+ expect(await spawn.stdout.transform(utf8.decoder).first, 'Hello');
+ });
+
+ test('.stderr is readable', () async {
+ final spawn = await processManager.spawn(
+ dart,
+ [p.join('test', '_files', 'stderr_hello.dart')],
+ );
+ expect(await spawn.stderr.transform(utf8.decoder).first, 'Hello');
+ });
+ });
+ });
+}
diff --git a/pkgs/io/test/shared_stdin_test.dart b/pkgs/io/test/shared_stdin_test.dart
new file mode 100644
index 0000000..71629ec
--- /dev/null
+++ b/pkgs/io/test/shared_stdin_test.dart
@@ -0,0 +1,80 @@
+// Copyright (c) 2017, 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:async';
+import 'dart:convert';
+
+import 'package:io/io.dart' hide sharedStdIn;
+import 'package:test/test.dart';
+
+void main() {
+ // ignore: close_sinks
+ late StreamController<String> fakeStdIn;
+ late SharedStdIn sharedStdIn;
+
+ setUp(() {
+ fakeStdIn = StreamController<String>(sync: true);
+ sharedStdIn = SharedStdIn(fakeStdIn.stream.map((s) => s.codeUnits));
+ });
+
+ test('should allow a single subscriber', () async {
+ final logs = <String>[];
+ final sub = sharedStdIn.transform(utf8.decoder).listen(logs.add);
+ fakeStdIn.add('Hello World');
+ await sub.cancel();
+ expect(logs, ['Hello World']);
+ });
+
+ test('should allow multiple subscribers', () async {
+ final logs = <String>[];
+ final asUtf8 = sharedStdIn.transform(utf8.decoder);
+ var sub = asUtf8.listen(logs.add);
+ fakeStdIn.add('Hello World');
+ await sub.cancel();
+ sub = asUtf8.listen(logs.add);
+ fakeStdIn.add('Goodbye World');
+ await sub.cancel();
+ expect(logs, ['Hello World', 'Goodbye World']);
+ });
+
+ test('should throw if a subscriber is still active', () async {
+ final active = sharedStdIn.listen((_) {});
+ expect(() => sharedStdIn.listen((_) {}), throwsStateError);
+ await active.cancel();
+ expect(() => sharedStdIn.listen((_) {}), returnsNormally);
+ });
+
+ test('should return a stream of lines', () async {
+ expect(
+ sharedStdIn.lines(),
+ emitsInOrder(<dynamic>[
+ 'I',
+ 'Think',
+ 'Therefore',
+ 'I',
+ 'Am',
+ ]),
+ );
+ [
+ 'I\nThink\n',
+ 'Therefore\n',
+ 'I\n',
+ 'Am\n',
+ ].forEach(fakeStdIn.add);
+ });
+
+ test('should return the next line', () {
+ expect(sharedStdIn.nextLine(), completion('Hello World'));
+ fakeStdIn.add('Hello World\n');
+ });
+
+ test('should allow listening for new lines multiple times', () async {
+ expect(sharedStdIn.nextLine(), completion('Hello World'));
+ fakeStdIn.add('Hello World\n');
+ await Future<void>.value();
+
+ expect(sharedStdIn.nextLine(), completion('Hello World'));
+ fakeStdIn.add('Hello World\n');
+ });
+}
diff --git a/pkgs/io/test/shell_words_test.dart b/pkgs/io/test/shell_words_test.dart
new file mode 100644
index 0000000..dc4441c
--- /dev/null
+++ b/pkgs/io/test/shell_words_test.dart
@@ -0,0 +1,185 @@
+// Copyright (c) 2017, 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 'package:io/io.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('shellSplit()', () {
+ group('returns an empty list for', () {
+ test('an empty string', () {
+ expect(shellSplit(''), isEmpty);
+ });
+
+ test('spaces', () {
+ expect(shellSplit(' '), isEmpty);
+ });
+
+ test('tabs', () {
+ expect(shellSplit('\t\t\t'), isEmpty);
+ });
+
+ test('newlines', () {
+ expect(shellSplit('\n\n\n'), isEmpty);
+ });
+
+ test('a comment', () {
+ expect(shellSplit('#foo bar baz'), isEmpty);
+ });
+
+ test('a mix', () {
+ expect(shellSplit(' \t\n# foo'), isEmpty);
+ });
+ });
+
+ group('parses unquoted', () {
+ test('a single token', () {
+ expect(shellSplit('foo'), equals(['foo']));
+ });
+
+ test('multiple tokens', () {
+ expect(shellSplit('foo bar baz'), equals(['foo', 'bar', 'baz']));
+ });
+
+ test('tokens separated by tabs', () {
+ expect(shellSplit('foo\tbar\tbaz'), equals(['foo', 'bar', 'baz']));
+ });
+
+ test('tokens separated by newlines', () {
+ expect(shellSplit('foo\nbar\nbaz'), equals(['foo', 'bar', 'baz']));
+ });
+
+ test('a token after whitespace', () {
+ expect(shellSplit(' \t\nfoo'), equals(['foo']));
+ });
+
+ test('a token before whitespace', () {
+ expect(shellSplit('foo \t\n'), equals(['foo']));
+ });
+
+ test('a token with a hash', () {
+ expect(shellSplit('foo#bar'), equals(['foo#bar']));
+ });
+
+ test('a token before a comment', () {
+ expect(shellSplit('foo #bar'), equals(['foo']));
+ });
+
+ test('dynamic shell features', () {
+ expect(
+ shellSplit(r'foo $(bar baz)'), equals(['foo', r'$(bar', 'baz)']));
+ expect(shellSplit('foo `bar baz`'), equals(['foo', '`bar', 'baz`']));
+ expect(shellSplit(r'foo $bar | baz'),
+ equals(['foo', r'$bar', '|', 'baz']));
+ });
+ });
+
+ group('parses a backslash', () {
+ test('before a normal character', () {
+ expect(shellSplit(r'foo\bar'), equals(['foobar']));
+ });
+
+ test('before a dynamic shell feature', () {
+ expect(shellSplit(r'foo\$bar'), equals([r'foo$bar']));
+ });
+
+ test('before a single quote', () {
+ expect(shellSplit(r"foo\'bar"), equals(["foo'bar"]));
+ });
+
+ test('before a double quote', () {
+ expect(shellSplit(r'foo\"bar'), equals(['foo"bar']));
+ });
+
+ test('before a space', () {
+ expect(shellSplit(r'foo\ bar'), equals(['foo bar']));
+ });
+
+ test('at the beginning of a token', () {
+ expect(shellSplit(r'\ foo'), equals([' foo']));
+ });
+
+ test('before whitespace followed by a hash', () {
+ expect(shellSplit(r'\ #foo'), equals([' #foo']));
+ });
+
+ test('before a newline in a token', () {
+ expect(shellSplit('foo\\\nbar'), equals(['foobar']));
+ });
+
+ test('before a newline outside a token', () {
+ expect(shellSplit('foo \\\n bar'), equals(['foo', 'bar']));
+ });
+
+ test('before a backslash', () {
+ expect(shellSplit(r'foo\\bar'), equals([r'foo\bar']));
+ });
+ });
+
+ group('parses single quotes', () {
+ test('that are empty', () {
+ expect(shellSplit("''"), equals(['']));
+ });
+
+ test('that contain normal characters', () {
+ expect(shellSplit("'foo'"), equals(['foo']));
+ });
+
+ test('that contain active characters', () {
+ expect(shellSplit("'\" \\#'"), equals([r'" \#']));
+ });
+
+ test('before a hash', () {
+ expect(shellSplit("''#foo"), equals([r'#foo']));
+ });
+
+ test('inside a token', () {
+ expect(shellSplit("foo'bar baz'qux"), equals([r'foobar bazqux']));
+ });
+
+ test('without a closing quote', () {
+ expect(() => shellSplit("'foo bar"), throwsFormatException);
+ });
+ });
+
+ group('parses double quotes', () {
+ test('that are empty', () {
+ expect(shellSplit('""'), equals(['']));
+ });
+
+ test('that contain normal characters', () {
+ expect(shellSplit('"foo"'), equals(['foo']));
+ });
+
+ test('that contain otherwise-active characters', () {
+ expect(shellSplit('"\' #"'), equals(["' #"]));
+ });
+
+ test('that contain escaped characters', () {
+ expect(shellSplit(r'"\$\`\"\\"'), equals([r'$`"\']));
+ });
+
+ test('that contain an escaped newline', () {
+ expect(shellSplit('"\\\n"'), equals(['']));
+ });
+
+ test("that contain a backslash that's not an escape", () {
+ expect(shellSplit(r'"f\oo"'), equals([r'f\oo']));
+ });
+
+ test('before a hash', () {
+ expect(shellSplit('""#foo'), equals([r'#foo']));
+ });
+
+ test('inside a token', () {
+ expect(shellSplit('foo"bar baz"qux'), equals([r'foobar bazqux']));
+ });
+
+ test('without a closing quote', () {
+ expect(() => shellSplit('"foo bar'), throwsFormatException);
+ expect(() => shellSplit(r'"foo bar\'), throwsFormatException);
+ });
+ });
+ });
+}