Charles has updated the proposal after reviewing community feedback. As with the first review, I'd like to focus specifically on these changes rather than revisiting the design discussions from beta.
All feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager by DM. When contacting the review manager directly, please include the proposal name in the subject line.
Things to consider
The goal of this feedback period is to improve the proposal through constructive discussion. Here are some questions to consider in your feedback:
How much effort did you put into your evaluation of this proposal?
If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
More information about Swift-Foundation evolution process is available at
The goal of this feedback period is to improve the proposal through constructive discussion.
The proposal could be improved by moving the set of global run() functions onto a type.
Currently, we still have a global run() function which
Contradicts Swift API design
Makes some forms of testing unnecessarily hard
Is unmotivated
Is hard to read. Even Subprocess's own codebase seems to prefer to essentially always write Subprocess.run(...) which is of course nicer. So let's do that.
We still have sixpublic func runs which are overloads. Normally, overloads are just a source of friction and odd problems with completion.
In this case however it makes dependency injection without wrapping the Subprocess module into a type:
You can pass a type that has six member functions called run
You can (awkwardly) pass a single global function run
You cannot pass around a bundle of six global run functions without bundling them into your own type. Let's have just one type for that and call it public struct|enum|whatever Subprocess
(I do very much disregard all 'having a global run() function makes Swift nice for shell script replacements'. This is not real, it doesn't look nearly as clean as a simple shell script and that also shouldn't be the goal. But if you really want that, you can make a SubprocessGlobal module that just defines a global function/variable called runSubprocess or run and forward)
It's hard to get a feel for the proposed changes without actually using them, so I won't comment on those.
I will say that in my usage of the 0.4-0.5 subprocess releases, I've had two problems which aren't addressed by the 1.0 proposal:
global run. I always want to call it as Subprocess.run because just run is so vague (and quite foreign to Swift, which has very few global functions). I agree with many of the previous comments in this and the previous 1.0 review thread: let's put run as a static in an empty Subprocess type.
too many overloads: basically it means that in any nested code (task groups, closures, etc.) any mistake in the arguments to run results in a generic error (failure to typecheck in a reasonable time, or an error misattributed to one of the enclosing functions). I know Swift tends to shy away from builders, because the flexibility of default arguments and ad-hoc overloading makes them less necessary than some other languages, but I think it could be worth reconsidering here.
Thanks for trying the previous versions. One of the changes in this proposal was about reducing the number of overloads. So if you get a chance please check it out
Also, reviewer manager hat off, I also wanted to chime in that I also write Subprocess.run, but it doesn't bother me that this is not enforced by the package. Also, putting a static func inside a scope, say a enum, still doesn't allow mocking for testing since it is effectively a global function. So, what problems does putting it inside a scope fix exactly?
You could do extension Subprocess: MyMockProtocol and then accept some MyMockProtocol.Type instead of directly referencing Subprocess. But modules cannot conform to protocols.
There's nothing stopping you from writing Subprocess.run, as demonstrated by subprocess’s own testing code. You can always write that if that's what you prefer, so why force everyone to write that when they don't have to?
This is not true. We never said we wanted global run() functions because we "want scripting to look nicer" (that was the argument for having Executable.path for convenience path lookup). We decided to use global functions because:
Subprocess has a very small and focused purpose: to launch processes with one of the run() functions. We started with Subprocess type and eventually decided to remove it when it became an empty wrapper just to hold static func run() with no instance method, no property, and no initializer. We don't see any benefit in having an empty namespace wrapper.
Subprocess is NOT in Foundation nor the Standard Library. You do not "accidentally" get these run() functions because someone else imported Foundation nor do you have these run() functions "polluting" the global namespace like global functions from the standard library, such as print.
You can absolutely write Subprocess.run if you prefer since the module acts like a perfectly fine namespace on its own already.
So far I have not received a convincing technical counterargument around why the points given above are invalid. We've also been using Subprocess ourselves and did not find global run() functions to be an issue so far. Most arguments against global run() function centered around "this is different from usual Swift API design". While I agree with this sentiment, I believe we've given enough technical argument on why we made this "exception".
However, I do not believe it is Subprocess's responsibility to solve that problem. Subprocess has a single, well-scoped job: providing the primitives needed to spawn child processes. It is the client's responsibility to wrap those primitives in whatever abstraction they see fit, asuch as protocol, wrapper type, etc., that suits their testing strategy. This is no different from how consumers of FileManager routinely introduce their own protocol for testing purposes; we would not expect FileManager itself to be a protocol just to accommodate that pattern.
The downstream client should perform wrapping and dependency injection in a way that fits their needs. For example, most clients only use one or two run() functions from Subprocess, so they would only need to wrap the ones they need instead of all six. Swiftly is a great example: it makes extensive use of Subprocess, but it's all funneled through one run().
This was one of the feedback items we received from the first review, and it has been addressed in 1.0.0-beta.1. Please see this section of the proposal.
Thank you to everyone who participated in the second review of SF-0037: Subprocess 1.0.
After discussion among workgroup members, we have decided to accept this proposal. Two topics that repeatedly came up in the discussions are the global run functions and the number of overloads.
On the staticrun function: The workgroup doesn't think converting the global run functions to static functions on a type would meaningfully improve the API design.
On the number of API overloads: Charles has already made significant progress in this area, reducing the overload count from 16 in earlier versions to 6 in the current proposal. The remaining Span-specific overloads serve as a necessary workaround for the current lack of support for capturing ~Copyable and ~Escapable types in closures. Once the compiler support is in place, we can revisit these and deprecate them if feasible.
We decided to accept it as 1.0 since there are no open questions or pending actions.
We expect that we will keep evolving the package, so there will always be opportunities to refine the API after this proposal is accepted.