Swift 6.2: Subprocess
Swift 6.2 introduces a new
Subprocesspackage that offers a streamlined, concurrency‑friendly API for launching and managing external processes.
The existing Foundation API for spawning a process,
NSTask, originated in Objective-C. It was subsequently renamed toProcessin Swift. As the language has continued to evolve,Processhas not kept up. It lacks support forasync/await, makes extensive use of completion handlers, and uses Objective-C exceptions to indicate developer error. This proposal introduces a new package calledSubprocess, which addresses the ergonomic shortcomings ofProcessand enhances the experience of using Swift for scripting and other areas such as server-side development.
It’s currently available as a package, rather than being built into the OS, so the API may not be fully stable, but you can use it on older OS versions.
One of the issues with NSTask/Process is that the simple API works for small amounts of input/output but (unbeknownst to many, even some experts) hangs once you exceed the OS’s buffer size. There are ways around this, but they are awkward (though you can hide them in a  helper class). As Christian Tietze (Mastodon) writes:
Helge Heß pointed out that naive usage of
Pipein childProcesses can break your program if you pipe too much data.[…]
If the data is larger than the pipe buffer, you need to drain the corresponding
FileHandlewith repeated read calls. (Or provide data larger than 64KiB with repeated write calls, respectively.)If you try to send/receive the whole buffer in one go, from a user’s perspective, your program will freeze, and the read call never return. As a CLI app, it’ll never terminate.
Subprocess seems to be designed to transparently handle this. You can specify the output size limit (and the encoding, if you want a string output), and it will collect the pieces of output as they arrive.
I’ve been using Swift to write scripts, and scripts often need to run shell commands. Unfortunately, Subprocess is currently cumbersome to use for scripting, as Jacob Bartlett writes:
These simple scripts have ludicrous amounts of overhead for what are trivial one-liner operations in the bash shell. Bear with me, because later we’ll look at where Swift subprocess can actually work well, in a more complex workflow.
The additional overhead of requiring a full SwiftPM project, compared to a Bash script, makes it incredibly cumbersome for simple workloads. Also, it’s still not an actual script, so it’ll always require a compilation (and potentially dependency resolution) overhead whenever your code changes.
On the other hand, the syntax, type safety, and composability of Swift code work pretty nicely when you have complex automation workflows to orchestrate, build, and run on demand.
Previously:
2 Comments RSS · Twitter · Mastodon
I don’t understand why the kiddies need to rewrite everything from scratch, rather than just fixing the existing API. At the end of the day, both implementations use syscalls. Is there really nobody at Apple that understands ObjC, has the attention span and mental capacity to non-destructively fix existing API and write a proper Swift wrapper for them? Instead, they end up with two, three, sometimes four different implementations they need to maintain, with the newest being just written in Swift, whoopdie doo. Until the next “sexy” thing pops up and the then-coon kiddies rewrite it all again. And on the way, they lost and semblance of stable API, so Swift wheel reinventions always get new functions that only work on latest OS (not in this case, for now).
SwiftPM ceremony apart, it looks cumbersome compared to Python:
from subprocess import run
p = run(['ls', '-l'], capture_output=True)
print(p.returncode, p.stdout, p.stderr)
And Python's subprocess handling is already much more cumbersome than the shell's. It doesn't look like you'll ever have a good time scripting in Swift.
In Python passing `check=True` to run() can also be used to fail early on a non-zero return code (like `set -e` in the shell), which is often what you want.







