From 8a614749363fcf7b3c1b10b6263818354f9ea214 Mon Sep 17 00:00:00 2001 From: Alexandre Roux Date: Mon, 15 Jan 2024 11:09:44 +0100 Subject: [PATCH] work on #103, add runSync and runExecutableArgumentsSync both to global space and Shell class --- packages/process_run/example/demo_sync.dart | 17 ++ packages/process_run/lib/process_run.dart | 2 - packages/process_run/lib/shell.dart | 7 +- packages/process_run/lib/src/process_run.dart | 100 +++++++++ packages/process_run/lib/src/shell.dart | 202 +++++++++++++++++- .../process_run/lib/src/shell_common.dart | 50 +++-- packages/process_run/test/echo_test.dart | 9 + packages/process_run/test/shell_api_test.dart | 3 + .../process_run/test/shell_common_test.dart | 15 ++ packages/process_run/test/shell_run_test.dart | 10 + packages/process_run/test/shell_test.dart | 11 + 11 files changed, 395 insertions(+), 31 deletions(-) create mode 100755 packages/process_run/example/demo_sync.dart diff --git a/packages/process_run/example/demo_sync.dart b/packages/process_run/example/demo_sync.dart new file mode 100755 index 0000000..80bb01a --- /dev/null +++ b/packages/process_run/example/demo_sync.dart @@ -0,0 +1,17 @@ +import 'dart:io'; + +import 'package:process_run/process_run.dart'; + +void main() { + // Run the command + runExecutableArgumentsSync('echo', ['hello world']); + + // Stream the out to stdout + runExecutableArgumentsSync('echo', ['hello world']); + + // Calling dart + runExecutableArgumentsSync('dart', ['--version'], verbose: true); + + // stream the output to stderr + runExecutableArgumentsSync('dart', ['--version'], stderr: stderr); +} diff --git a/packages/process_run/lib/process_run.dart b/packages/process_run/lib/process_run.dart index 7a2e416..8f3759d 100644 --- a/packages/process_run/lib/process_run.dart +++ b/packages/process_run/lib/process_run.dart @@ -7,6 +7,4 @@ export 'package:process_run/src/shell_utils_common.dart' show argumentsToString, argumentToString; export 'shell.dart'; -export 'src/process_run.dart' - show runExecutableArguments, executableArgumentsToString; export 'which.dart' show which, whichSync; diff --git a/packages/process_run/lib/shell.dart b/packages/process_run/lib/shell.dart index 65ce319..6f9c509 100644 --- a/packages/process_run/lib/shell.dart +++ b/packages/process_run/lib/shell.dart @@ -55,9 +55,12 @@ export 'src/process_cmd.dart' /// Deprecated ProcessCmd; export 'src/process_run.dart' - show runExecutableArguments, executableArgumentsToString; + show + runExecutableArguments, + executableArgumentsToString, + runExecutableArgumentsSync; export 'src/prompt.dart' show promptConfirm, promptTerminate, prompt; -export 'src/shell.dart' show run, Shell, ShellException; +export 'src/shell.dart' show run, runSync, Shell, ShellException; export 'src/shell_environment.dart' show ShellEnvironment, diff --git a/packages/process_run/lib/src/process_run.dart b/packages/process_run/lib/src/process_run.dart index 9955640..35438a5 100644 --- a/packages/process_run/lib/src/process_run.dart +++ b/packages/process_run/lib/src/process_run.dart @@ -189,6 +189,106 @@ Future runExecutableArguments( return result; } +/// +/// if [commandVerbose] or [verbose] is true, display the command. +/// if [verbose] is true, stream stdout & stdin +/// +/// Compared to the async version, it is not possible to kill the spawn process nor to +/// feed any input. +ProcessResult runExecutableArgumentsSync( + String executable, List arguments, + {String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool? runInShell, + Encoding? stdoutEncoding = systemEncoding, + Encoding? stderrEncoding = systemEncoding, + StreamSink>? stdout, + StreamSink>? stderr, + bool? verbose, + bool? commandVerbose}) { + if (verbose == true) { + commandVerbose = true; + stdout ??= io.stdout; + stderr ??= io.stderr; + } + + if (commandVerbose == true) { + utils.streamSinkWriteln(stdout ?? io.stdout, + '\$ ${executableArgumentsToString(executable, arguments)}', + encoding: stdoutEncoding); + } + + // Build our environment + var shellEnvironment = ShellEnvironment.full( + environment: environment, + includeParentEnvironment: includeParentEnvironment); + + // Default is the full command + var executableShortName = executable; + + // Find executable if needed, i.e. if it is only a name + if (basename(executable) == executable) { + // Try to find it in path or use it as is + executable = utils.findExecutableSync(executable, shellEnvironment.paths) ?? + executable; + } else { + // resolve locally + executable = utils.findExecutableSync(basename(executable), [ + join(workingDirectory ?? Directory.current.path, dirname(executable)) + ]) ?? + executable; + } + + // Fix runInShell on windows (force run in shell for non-.exe) + runInShell = utils.fixRunInShell(runInShell, executable); + + io.ProcessResult result; + try { + result = Process.runSync( + executable, + arguments, + environment: shellEnvironment, + includeParentEnvironment: false, + runInShell: runInShell, + workingDirectory: workingDirectory, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding, + ); + } catch (e) { + if (verbose == true) { + io.stderr.writeln(e); + io.stderr.writeln( + '\$ ${executableArgumentsToString(executableShortName, arguments)}'); + io.stderr.writeln( + 'workingDirectory: ${workingDirectory ?? Directory.current.path}'); + } + rethrow; + } + + List outputToIntList(dynamic data, Encoding? encoding) { + if (data is List) { + return data; + } else if (data is String && encoding != null) { + return encoding.encode(data); + } else { + throw 'Unexpected data type: ${data.runtimeType}'; + } + } + + if (stdout != null) { + var out = outputToIntList(result.stdout, stdoutEncoding); + stdout.add(out); + } + + if (stderr != null) { + var err = outputToIntList(result.stderr, stderrEncoding); + stderr.add(err); + } + + return result; +} + /// Command runner. not exported /// Execute a predefined ProcessCmd command diff --git a/packages/process_run/lib/src/shell.dart b/packages/process_run/lib/src/shell.dart index 65322af..69312ed 100644 --- a/packages/process_run/lib/src/shell.dart +++ b/packages/process_run/lib/src/shell.dart @@ -3,11 +3,12 @@ import 'dart:io' as io; import 'package:path/path.dart'; import 'package:process_run/shell.dart'; +import 'package:process_run/shell.dart' as impl; import 'package:process_run/src/bin/shell/import.dart'; import 'package:process_run/src/platform/platform.dart'; import 'package:process_run/src/process_run.dart'; import 'package:process_run/src/shell_common.dart' - show ShellCore, ShellOptions, shellDebug; + show ShellCore, ShellCoreSync, ShellOptions, shellDebug; import 'package:process_run/src/shell_utils.dart'; import 'package:synchronized/synchronized.dart'; @@ -16,7 +17,7 @@ export 'shell_common.dart' show shellDebug; /// /// Run one or multiple plain text command(s). /// -/// Commands can be splitted by line. +/// Commands can be split by line. /// /// Commands can be on multiple line if ending with ' ^' or ' \'. /// @@ -68,6 +69,62 @@ Future> run( .run(script, onProcess: onProcess); } +/// +/// Run one or multiple plain text command(s). +/// +/// Commands can be split by line. +/// +/// Commands can be on multiple line if ending with ' ^' or ' \'. +/// +/// Returns a list of executed command line results. Verbose by default. +/// +/// +/// ```dart +/// await run('flutter build'); +/// await run('dart --version'); +/// await run(''' +/// dart --version +/// git status +/// '''); +/// +/// Compared to the async version, it is not possible to kill the spawn process nor to +/// feed any input. +/// ``` +List runSync( + String script, { + bool throwOnError = true, + String? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + bool? runInShell, + Encoding stdoutEncoding = systemEncoding, + Encoding stderrEncoding = systemEncoding, + StreamSink>? stdout, + StreamSink>? stderr, + bool verbose = true, + + // Default to true + bool? commandVerbose, + // Default to true if verbose is true + bool? commentVerbose, +}) { + return Shell( + throwOnError: throwOnError, + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding, + stdin: stdin, + stdout: stdout, + stderr: stderr, + verbose: verbose, + commandVerbose: commandVerbose, + commentVerbose: commentVerbose) + .runSync(script); +} + /// Multiplatform Shell utility to run a script with multiple commands. /// /// Extra path/env can be loaded using ~/.config/tekartik/process_run/env.yaml @@ -89,7 +146,7 @@ Future> run( /// /// A list of ProcessResult is returned /// -abstract class Shell implements ShellCore { +abstract class Shell implements ShellCore, ShellCoreSync { final ShellOptions _options; /// Incremental internal runId @@ -279,7 +336,7 @@ abstract class Shell implements ShellCore { /// /// Run one or multiple plain text command(s). /// - /// Commands can be splitted by line. + /// Commands can be split by line. /// /// Commands can be on multiple line if ending with ' ^' or ' \'. (note that \ /// must be escaped too so you might have to enter \\). @@ -329,6 +386,55 @@ abstract class Shell implements ShellCore { }); } + /// + /// Run one or multiple plain text command(s). + /// + /// Commands can be split by line. + /// + /// Commands can be on multiple line if ending with ' ^' or ' \'. (note that \ + /// must be escaped too so you might have to enter \\). + /// + /// Returns a list of executed command line results. + /// + /// [onProcess] is called for each started process. + /// + /// Compare to the async version, it is not possible to kill the spawn process nor to + /// feed any input. + /// + @override + List runSync( + String script, + ) { + var commands = scriptToCommands(script); + + var processResults = []; + for (var command in commands) { + // Display the comments + if (isLineComment(command!)) { + if (_options.commentVerbose) { + stdout.writeln(command); + } + continue; + } + var parts = shellSplit(command); + var executable = parts[0]; + var arguments = parts.sublist(1); + + // Find alias + var alias = _options.environment.aliases[executable]; + if (alias != null) { + // The alias itself should be split + parts = shellSplit(alias); + executable = parts[0]; + arguments = [...parts.sublist(1), ...arguments]; + } + var processResult = runExecutableArgumentsSync(executable, arguments); + processResults.add(processResult); + } + + return processResults; + } + final _runLock = Lock(); /// Run a single [executable] with [arguments], resolving the [executable] if needed. @@ -346,6 +452,24 @@ abstract class Shell implements ShellCore { }); } + /// Run a single [executable] with [arguments], resolving the [executable] if needed. + /// + /// Returns a process result (or throw if specified in the shell). + /// + /// [onProcess] is called for each started process. + @override + ProcessResult runExecutableArgumentsSync( + String executable, + List arguments, + ) { + var runId = ++_runId; + return _runExecutableArgumentsSync( + runId, + executable, + arguments, + ); + } + Future _runLocked(FutureOr Function(int runId) action) { // devPrint('Previous: ${_currentProcessToString()}'); var runId = ++_runId; @@ -373,6 +497,76 @@ abstract class Shell implements ShellCore { _currentProcessResultCompleter = null; } + /// Run a single [executable] with [arguments], resolving the [executable] if needed. + /// + /// Call onProcess upon process startup + /// + /// Returns a process result (or throw if specified in the shell). + ProcessResult _runExecutableArgumentsSync( + int runId, + String executable, + List arguments, + ) { + var executableFullPath = + findExecutableSync(executable, _userPaths) ?? executable; + var processCmd = ProcessCmd(executableFullPath, arguments); + try { + _clearPreviousContext(); + + ProcessResult? processResult; + + try { + if (shellDebug) { + print('$_runId: Before $processCmd'); + } + + processResult = impl.runExecutableArgumentsSync( + executableFullPath, arguments, + runInShell: _options.runInShell, + environment: _options.environment, + includeParentEnvironment: false, + stderrEncoding: _options.stderrEncoding ?? io.systemEncoding, + stdoutEncoding: _options.stdoutEncoding ?? io.systemEncoding, + workingDirectory: _options.workingDirectory); + } finally { + if (shellDebug) { + print( + '$_runId: After $executableFullPath exitCode ${processResult?.exitCode}'); + } + } + // devPrint('After $processCmd'); + if (_options.throwOnError && processResult.exitCode != 0) { + throw ShellException( + '$processCmd, exitCode ${processResult.exitCode}, workingDirectory: $_workingDirectoryPath', + processResult); + } + return processResult; + } on ProcessException catch (e) { + var stderr = _options.stderr ?? io.stderr; + void writeln([String? msg]) { + stderr.add(utf8.encode(msg ?? '')); + stderr.add(utf8.encode('\n')); + } + + var workingDirectory = + _options.workingDirectory ?? Directory.current.path; + + writeln(); + if (!Directory(workingDirectory).existsSync()) { + writeln('Missing working directory $workingDirectory'); + } else { + writeln(''' + Check that $executableFullPath exists + command: $processCmd'''); + } + writeln(); + + throw ShellException( + '$processCmd, error: $e, workingDirectory: $_workingDirectoryPath', + null); + } + } + /// Run a single [executable] with [arguments], resolving the [executable] if needed. /// /// Call onProcess upon process startup diff --git a/packages/process_run/lib/src/shell_common.dart b/packages/process_run/lib/src/shell_common.dart index 540f7c1..84dd379 100644 --- a/packages/process_run/lib/src/shell_common.dart +++ b/packages/process_run/lib/src/shell_common.dart @@ -36,28 +36,6 @@ var shellDebug = false; // devWarning(true); // false /// A list of ProcessResult is returned /// abstract class ShellCore { - /* - ShellCore( - {ShellOptions? options, - Map? environment, - - /// Compat, prefer options - bool? verbose, - Encoding? stdoutEncoding, - Encoding? stderrEncoding, - StreamSink>? stdout, - StreamSink>? stderr, - bool? runInShell}) => - shellContext.newShell( - options: options?.clone( - verbose: verbose, - stderrEncoding: stderrEncoding, - stdoutEncoding: stdoutEncoding, - runInShell: runInShell, - stdout: stdout, - stderr: stderr), - environment: environment);*/ - /// Kills the current running process. /// /// Returns `true` if the signal is successfully delivered to the process. @@ -116,6 +94,32 @@ abstract class ShellCore { ShellContext get context; } +abstract class ShellCoreSync { + /// + /// Run one or multiple plain text command(s). + /// + /// Commands can be split by line. + /// + /// Commands can be on multiple line if ending with ' ^' or ' \'. (note that \ + /// must be escaped too so you might have to enter \\). + /// + /// Returns a list of executed command line results. + /// + /// Compared to the async version, it is not possible to kill the spawn process nor to + /// feed any input. + /// + List runSync(String script); + + /// Run a single [executable] with [arguments], resolving the [executable] if needed. + /// + /// Returns a process result (or throw if specified in the shell). + /// + /// Compared to the async version, it is not possible to kill the spawn process nor to + /// feed any input. + ProcessResult runExecutableArgumentsSync( + String executable, List arguments); +} + /// Shell options. class ShellOptions { final bool _throwOnError; @@ -248,7 +252,7 @@ Future which(String command, } /// Default missing implementation. -mixin ShellMixin implements ShellCore { +mixin ShellMixin implements ShellCore, ShellCoreSync { // Set lazily after newShell; @override late ShellContext context; diff --git a/packages/process_run/test/echo_test.dart b/packages/process_run/test/echo_test.dart index 6ce3908..863693e 100644 --- a/packages/process_run/test/echo_test.dart +++ b/packages/process_run/test/echo_test.dart @@ -52,6 +52,15 @@ void main() { stderrEncoding: stderrEncoding, stdout: stdout); check(result); + result = runExecutableArgumentsSync(executable, arguments, + workingDirectory: workingDirectory, + environment: environment, + includeParentEnvironment: includeParentEnvironment, + runInShell: runInShell, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding, + stdout: stdout); + check(result); } test('stdout', () async { diff --git a/packages/process_run/test/shell_api_test.dart b/packages/process_run/test/shell_api_test.dart index f6f58d7..9dfd5a4 100644 --- a/packages/process_run/test/shell_api_test.dart +++ b/packages/process_run/test/shell_api_test.dart @@ -36,6 +36,9 @@ void main() { promptTerminate; prompt; run; + runSync; + runExecutableArguments; + runExecutableArgumentsSync; Shell; ShellOptions; ShellException; diff --git a/packages/process_run/test/shell_common_test.dart b/packages/process_run/test/shell_common_test.dart index 110dc5b..fffbc04 100644 --- a/packages/process_run/test/shell_common_test.dart +++ b/packages/process_run/test/shell_common_test.dart @@ -152,6 +152,21 @@ class ShellMock with ShellMixin implements Shell { throw UnimplementedError(); } + @override + ProcessResult runExecutableArgumentsSync( + String executable, + List arguments, + ) { + // TODO: implement runExecutableArguments + throw UnimplementedError(); + } + + @override + List runSync(String script) { + // TODO: implement runSync + throw UnimplementedError(); + } + @override late final ShellOptions options; } diff --git a/packages/process_run/test/shell_run_test.dart b/packages/process_run/test/shell_run_test.dart index cbb83a7..aaf6a24 100644 --- a/packages/process_run/test/shell_run_test.dart +++ b/packages/process_run/test/shell_run_test.dart @@ -45,6 +45,15 @@ void main() { verbose: false); }); + test('sync --version', () async { + var result = runSync('dart --version', + throwOnError: false, verbose: false, commandVerbose: true) + .first; + stdout.writeln('stdout: ${result.stdout.toString().trim()}'); + stdout.writeln('stderr: ${result.stderr.toString().trim()}'); + stdout.writeln('exitCode: ${result.exitCode}'); + }); + test('--version', () async { for (var bin in [ // 'dartdoc', deprecated @@ -63,6 +72,7 @@ void main() { stdout.writeln('exitCode: ${result.exitCode}'); } }); + test('dart compile', () async { var bin = 'build/native/info.exe'; await Directory(dirname(bin)).create(recursive: true); diff --git a/packages/process_run/test/shell_test.dart b/packages/process_run/test/shell_test.dart index b62b3e0..4cb1ebe 100644 --- a/packages/process_run/test/shell_test.dart +++ b/packages/process_run/test/shell_test.dart @@ -162,6 +162,12 @@ dart example/echo.dart -o ${shellArgument(weirdText)} expect(results.length, 1); expect(results.first.exitCode, 0); }); + test('sync dart', () async { + var shell = Shell(verbose: debug); + var results = shell.runSync('''dart --version'''); + expect(results.length, 1); + expect(results.first.exitCode, 0); + }); test('dart runExecutableArguments', () async { var shell = Shell(verbose: debug); @@ -585,6 +591,10 @@ _tekartik_dummy_app_that_does_not_exits (await sh.runExecutableArguments(dartExecutable!, ['--version'])) .stderr .toString()); + var resolvedVersionSync = parseDartBinVersionOutput( + (sh.runExecutableArgumentsSync(dartExecutable!, ['--version'])) + .stderr + .toString()); var whichVersion = parseDartBinVersionOutput( (await sh.runExecutableArguments(whichDart!, ['--version'])) .stderr @@ -595,6 +605,7 @@ _tekartik_dummy_app_that_does_not_exits .toString()); expect(version, resolvedVersion); expect(version, whichVersion); + expect(version, resolvedVersionSync); }); test('verbose non ascii char', () async {