I fear this will get an ever growing tree of options and arguments. Note that shell/1,2 uses the system command interpreter, in introducing the necessary portability and quoting issues. You may or may not want using the shell. process_create/3 calls the process itself and provides safe passing of arguments (except for Windows as the underlying API passes the commandline as a single string).
For library(git), which provides some abstraction over running git, notably for the pack manager, I have defined git_process_output/3. This takes as argument a closure that is called with the output stream as created by create_process/3. That is a reasonable API for a lot of stuff, but probably again too complicated.
I’m still in doubt between the idea that there are simply too many options and applications should make up their mind and define a couple of helpers to get what they need, e.g.
shell_output(Command, String) :-
setup_call_cleanup(
process_create('/bin/sh', ['-c', Command],
[ stdout(pipe(Out))
]),
read_string(Out, _, String),
close(Out)).
This is a bit annoying if you just want to write a half page script to do something useful, but it is IMO a bearable overhead for most more serious applications.
On the other handle, there are complicated scenarios were we need threads and/or I/O multiplexing. One would like to have this complicated stuff implemented by knowledgeable people and reused from a library. Unfortunately, the list of scenarios is large.
P.s. Note that SWI-Prolog also offers open/3,4 with pipe(Command) as “filename”. That is a little shorter. It is not portable though and it makes decisions for you that you may or may not like while you can be explicit about these in the example above.
Luckily, if we ignore the exit code when using process_create/3, one gets an exception if the process terminates with non-zero exit or due to a signal.