In a Dart command-line tool that shells out to git, when do you use Process.run versus Process.start, and why can a started child process hang?
answer
- collect everything versus live streams
- ProcessResult: exitCode, stdout, stderr
- non-zero exit is not an exception
- pipes have limited capacity
- runInShell defaults to false
basics
~20 sProcess.run waits for the child to exit and returns a ProcessResult with exitCode, stdout and stderr. Process.start returns a live Process with stdin, stdout and stderr streams; it hangs if you never drain stdout or stderr.
solid answer
~40 s`Process.run(executable, args, workingDirectory: ...)` fits a call like `git rev-parse HEAD`: small output, and you only need the result. It returns a `ProcessResult` whose `stdout` and `stderr` are decoded with `systemEncoding` into `String`s (or `Uint8List` if you pass a `null` encoding). A non-zero exit is not an exception - check `exitCode` yourself; only a failure to start throws `ProcessException`. `Process.start` suits long or large output (`git log` over a big repo), live progress, writing to the child's `stdin`, or `kill()`. The catch: the pipes have limited capacity, so if you await `exitCode` without reading `stdout` and `stderr`, the child blocks writing and never exits. `runInShell` defaults to `false`, so the argument list reaches git without shell parsing.
code
dart · 13 linesimport 'dart:io';
Future<String> headCommit(String repo) async {
final result = await Process.run(
'git',
['rev-parse', 'HEAD'],
workingDirectory: repo,
);
if (result.exitCode != 0) {
throw StateError('git rev-parse failed: ${result.stderr}');
}
return (result.stdout as String).trim();
}go deeper
Recall that Process.run gives you exitCode, stdout and stderr after the command finishes.
Explain the ProcessResult types, that a non-zero exit is not an exception, and when you need Process.start's streams.
Diagnose the full-pipe hang, drain both streams, avoid shell parsing of untrusted arguments and interpret signal exit codes.
Decide how a tool wraps external commands: error reporting, timeouts, cross-platform executable resolution and when to avoid shelling out.
## Two ways to run another program `dart:io` exposes child processes through the `Process` class. A command-line tool written in Dart - say, one that reads files in a repository and asks `git` about them - usually needs one of two shapes. | | `Process.run` | `Process.start` | |---|---|---| | Returns | `Future<ProcessResult>` after the child exits | `Future<Process>` as soon as it starts | | Output | whole `stdout`/`stderr`, decoded | live `Stream<List<int>>` for each | | Input | none | `stdin` as an `IOSink` | | Control | none | `pid`, `kill()`, `exitCode` future | | Best for | short commands with small output | long runs, large output, interaction | There is also `Process.runSync`, which takes the same arguments as `run` but blocks the isolate until the child finishes. ## Process.run: collect and inspect `Process.run` accepts `workingDirectory`, `environment`, `includeParentEnvironment` (default `true`), `runInShell` (default `false`) and `stdoutEncoding`/`stderrEncoding` (default `systemEncoding`). The `ProcessResult` holds `exitCode`, `stdout` and `stderr`; with the default encodings the outputs are `String`s, and passing `null` gives raw `Uint8List` bytes. Two behaviours catch people out: - **A failing command does not throw.** If `git` exits with status 128 because the directory is not a repository, `run` completes normally with `exitCode == 128`. You must check it. - **A command that cannot start does throw.** If `git` is not on the `PATH`, the Future completes with a `ProcessException`. ## Process.start: streams and the pipe trap `Process.start` returns a `Process` whose `stdout` and `stderr` are byte streams and whose `stdin` is a sink. This is how you process `git log` for a large history line by line, show progress while a clone runs, or feed input to a command. The documentation spells out the trap: `stdin`, `stdout` and `stderr` are **pipes with limited capacity**. If the child writes more than the pipe holds and nobody reads it, the child **blocks** waiting for space. So code that does `await process.exitCode` without consuming `stdout` - or that reads `stdout` but ignores a chatty `stderr` - can wait forever. The docs also require reading all data on both streams so system resources are released. Rules that follow: 1. Always start consuming **both** `stdout` and `stderr` before awaiting `exitCode`. 2. `exitCode` may complete before the output streams have delivered their last bytes, so wait for the streams' completion too if you need all output. 3. Use `ProcessStartMode.inheritStdio` when you simply want the child's output to appear in your terminal; `ProcessStartMode.detached` gives you only the `pid`, and reading `exitCode` then throws a `StateError`. ## Arguments, shells and exit codes - With `runInShell: false`, the list of arguments is passed to the executable as-is, so a branch name containing spaces or shell metacharacters is safe. Setting `runInShell: true` routes through `/bin/sh` or `cmd.exe`, which reintroduces quoting and injection concerns. On Windows, `.bat` and `.cmd` files may be run through a shell regardless. - On Linux and macOS, a child killed by a signal reports a **negative** exit code whose absolute value is the signal number. - `kill()` sends `ProcessSignal.sigterm` by default. ## Common mistakes - Treating `ProcessResult` as success without checking `exitCode`. - Using `Process.start` and awaiting only `exitCode`. - Building a single command string and setting `runInShell: true` to split it. - Calling `runSync` inside a Flutter desktop app's UI isolate.
- What happens if git is not installed when you call Process.run('git', ...)?The child cannot be started, so the returned Future completes with a `ProcessException` carrying the executable, arguments and OS message. That is different from git starting and failing, which completes normally with a non-zero `exitCode`.
- Why is runInShell: true risky when an argument comes from user input?With a shell, the command line is parsed by `/bin/sh` or `cmd.exe`, so characters such as `;`, `&` or backticks in the input can start other commands. With the default `false`, each list element reaches the executable as one argument and no shell parsing happens.
- What does a negative exitCode from a Process mean on Linux or macOS?The child was terminated by a signal; the absolute value is the signal number, so -11 means SIGSEGV and -15 means SIGTERM. A normal exit on those systems is in the range 0 to 255.
saying these in an interview costs you the question
- Assumes Process.run throws when the command exits non-zero
- Awaits exitCode after Process.start without reading stdout or stderr
- Sets runInShell: true just to split a command string
- Thinks the child is killed automatically when its output is ignored
- Believes ProcessResult.stdout is always a String regardless of encoding