3

I'm trying to pipe output from echo into a command using GLib's spawn_command_line_sync method. The problem I've run into is echo is interpreting the entire command as the argument.

To better explain, I run this in my code:

string command = "echo \"" + some_var + "\" | command";
Process.spawn_command_line_sync (command.escape (), 
                                 out r, out e, out s);

I would expect the variable to be echoed to the pipe and the command run with the data piped, however when I check on the result it's just echoing everything after echo like this:

"some_var's value" | command

I think I could just use the Posix class to run the command but I like having the result, error and status values to listen to that the spawn_command_line_sync method provides.

Plays2
  • 1,115
  • 4
  • 12
  • 23
  • Possible duplicate of [Redirecting output of an external application started with glib](https://stackoverflow.com/questions/14800369/redirecting-output-of-an-external-application-started-with-glib) – Jens Mühlenhoff Jan 26 '19 at 20:17

4 Answers4

5

The problem is that you are providing shell syntax to what is essentially the kernel’s exec() syscall. The shell pipe operator redirects the stdout of one process to the stdin of the next. To implement that using Vala, you need to get the file descriptor for the stdin of the command process which you’re running, and write some_var to it manually.

Philip Withnall
  • 5,293
  • 14
  • 28
  • Yeah I was hoping I could avoid using `spawn_async_with_pipes` here but I guess I can't. I'll update this with the full implementation in a bit, thanks. – Plays2 Jan 26 '19 at 20:28
5

You are combining two subprocesses into one. Instead echo and command should be treated separately and have a pipe set up between them. For some reason many examples on Stack Overflow and other sites use the Process.spawn_* functions, but using GSubprocess is an easier syntax.

This example pipes the output of find . to sort and then prints the output to the console. The example is a bit longer because it is a fully working example and makes use of a GMainContext for asynchronous calls. GMainContext is used by GMainLoop, GApplication and GtkApplication:

void main () {
    var mainloop = new MainLoop ();
    SourceFunc quit = ()=> {
        mainloop.quit ();
        return Source.REMOVE;
    };
    read_piped_commands.begin ("find .", "sort", quit);
    mainloop.run ();
}

async void read_piped_commands (string first_command, string second_command, SourceFunc quit) {
    var output = splice_subprocesses (first_command, second_command);
    try {
        string? line = null;
        do {
            line = yield output.read_line_async ();
            print (@"$(line ?? "")\n");
            }
        while (line != null);
    } catch (Error error) {
        print (@"Error: $(error.message)\n");
    }
    quit ();
}

DataInputStream splice_subprocesses (string first_command, string second_command) {
    InputStream end_pipe = null;
    try {
        var first = new Subprocess.newv (first_command.split (" "), STDOUT_PIPE);
        var second = new Subprocess.newv (second_command.split (" "), STDIN_PIPE | STDOUT_PIPE);

        second.get_stdin_pipe ().splice (first.get_stdout_pipe (), CLOSE_TARGET);
        end_pipe = second.get_stdout_pipe ();
    } catch (Error error) {
        print (@"Error: $(error.message)\n");
    }
    return new DataInputStream (end_pipe);
}

It is the splice_subprocesses function that answers your question. It takes the STDOUT from the first command as an InputStream and splices it with the OutputStream (STDIN) for the second command.

The read_piped_commands function takes the output from the end of the pipe. This is an InputStream that has been wrapped in a DataInputStream to give access to the read_line_async convenience method.

aggsol
  • 2,343
  • 1
  • 32
  • 49
AlThomas
  • 4,169
  • 12
  • 22
3

Here's the full, working implementation:

try {
    string[] command = {"command", "-options", "-etc"};
    string[] env = Environ.get ();
    Pid child_pid;
    string some_string = "This is what gets piped to stdin"

    int stdin;
    int stdout;
    int stderr;

    Process.spawn_async_with_pipes ("/",
        command,
        env,
        SpawnFlags.SEARCH_PATH | SpawnFlags.DO_NOT_REAP_CHILD,
        null,
        out child_pid,
        out stdin,
        out stdout,
        out stderr);

    FileStream input = FileStream.fdopen (stdin, "w");
    input.write (some_string.data);

    /* Make sure we close the process using it's pid */
    ChildWatch.add (child_pid, (pid, status) => {
        Process.close_pid (pid);
    });
} catch (SpawnError e) {
    /* Do something w the Error */
}

I guess playing with the FileStream is what really made it hard to figure this out. Turned out to be pretty straightforward.

Plays2
  • 1,115
  • 4
  • 12
  • 23
0

Based on previous answers probably an interesting case is to use program arguments to have a general app to pipe any input on it:

pipe.vala:

void main (string[] args) {
    try {
        string command = args[1];
        var subproc = new Subprocess(STDIN_PIPE | STDOUT_PIPE, command);

        var data = args[2].data;
        var input = new MemoryInputStream.from_data(data, GLib.free);

        subproc.get_stdin_pipe ().splice (input, CLOSE_TARGET);
        var end_pipe = subproc.get_stdout_pipe ();
        var output = new DataInputStream (end_pipe);

        string? line = null;
        do {
            line = output.read_line();
            print (@"$(line ?? "")\n");
        } while (line != null);
    } catch (Error error) {
        print (@"Error: $(error.message)\n");
    }
}

build:

$ valac --pkg gio-2.0 pipe.vala

and run:

$ ./pipe sort "cc
ab
aa
b
"

Output:

aa
ab
b
cc
albfan
  • 12,542
  • 4
  • 61
  • 80