Capturing shell output

Hi!

Is there an API in SWI-Prolog that would be something between shell/2 and process_create/3 in terms of complexity? All I want to is run some commands and capture the output, the process_create dance with setting up input, output then reading those is bit too much of an overhead for such a simple, and in my opinion, relatively common task. For example in Ruby, it’s so common that it’s as easy as just quoting the command in backticks (e.g. user = `whoami`).

It is still a one-liner :slight_smile:

?- setup_call_cleanup(process_create(path(whoami), [], [stdout(pipe(Out))]), read_string(Out, _, Str), close(Out)).
Out = <stream>(0x556dc2d65e00),
Str = "janw\n".

AFAIK there are various packages that provide such abstractions. All is simple as long as there is one input or output that we want to connect to Prolog. If we want to handle multiple streams we must make sure we keep draining the output stream(s). Failure to to do so may fill the process’ output buffer and cause the process to suspend and the whole thing to deadlock. Then it gets harder as we need I/O multiplexing using wait_for_input/3 or we need to use threads. This notably gets complicated if we want to cleanup everything cleanly in all error scenarios.

Possibly we should add a library(process_util), similar to thread_util to deal with some of these common scenarios? What are the common scenarios though? Note that even above we may or may not want to remove white space or get the result as atom, string, codes or chars.

Together with the portability problems, quoting problems and typically rather poor performance has been a motivation to offer as much as possible functionality through libraries rather then by calling processes. That is why there are bindings for libarchive, zlib, a lot if the file primitives, etc. Note that Janus may also allow to do a lot of this stuff through Python.

Maybe not the answer you were looking for …

What about:

  • Exit code - only provided at the end. Normally 0 for success, but grep returns 1 if no match, which might or might not be acceptable also.
  • Do you want to stream the output (e.g. piping through gzip), or wait for the definiteness of the exit code first?
  • STDERR output, if any (rather than just STDOUT)
  • For completeness - might want to be piping via STDIN to e.g. gzip

I gave some basic predicates at How to get shell echo in Prolog variables - #15 by brebs

Haven’t looked at it since. This could do with a standard library, sure.

I do agree that it’s not too hard to do process_create and have full control about all details. It’s just that many times while using SWI-Prolog to script things I just want the default case of reading the output from stdout and not really handling errors. A quick github search for “process_create lang:prolog” reveals mostly this usage pattern. I think natural place for this kind of behaviour would be shell/3 as we probably wouldn’t want to break compatibiliy of shell/2 which could return both error code and output of the command.

Can’t write a professional-quality app without sensible error-handling.

Anyway, in the code I linked to, you want ExitCode to unify with exit(0) - easy.

The exit code should never be ignored - it indicates whether the process’ output is complete.

1 Like

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.

1 Like

if you are after convenience or elegance, you might be interested in pack(by_unix).

άμπελος;src/by_unix> swipl -q -f none
?- use_module(library(by_unix)).
true.

?- Lines @@ ls().
Lines = [doc, ‘pack.pl’, prolog, scripts].

hope it entertains if not help,

Nicos Angelopoulos

https://stoics.org.uk/~nicos

1 Like