8

Is there a way to only execute a command after another is done without a temp file? I have one longer running command and another command that formats the output and sends it to a HTTP server using curl. If i just execute commandA | commandB, commandB will start curl, connect to the server and start sending data. Because commandAtakes so long, the HTTP server will timeout. I can do what I want with commandA > /tmp/file && commandB </tmp/file && rm -f /tmp/file

Out of curiosity I want to know if there is a way to do it without the temp file. I tried mbuffer -m 20M -q -P 100 but the curl process is still started right at the beginning. Mbuffer waits just until commandAis done with actually sending the data. (The data itself is just a few hundred kb at max)

  • What about commandA && commandB? – eyoung100 Mar 4 '15 at 17:18
  • 1
    that doesn't transmit the output of commandAto commandB, does it? – Josef Mar 4 '15 at 17:30
  • It starts Command B if Command A completes successfully, which would mean that curl doesn't start early. – eyoung100 Mar 4 '15 at 17:33
  • 2
    @eyoung100 but it doesn't pass stdout from commandA to stdin for commandB, which is what Josef needs! – roaima Mar 4 '15 at 17:34
  • If he wants output passed, he has to use a file. See cuonglm's answer. – eyoung100 Mar 4 '15 at 17:37
10

This is similar to a couple of the other answers.  If you have the “moreutils” package, you should have the sponge command.  Try

commandA | sponge | { IFS= read -r x; { printf "%s\n" "$x"; cat; } | commandB; }

The sponge command is basically a pass-through filter (like cat) except that it does not start writing the output until it has read the entire input.  I.e., it “soaks up” the data, and then releases it when you squeeze it (like a sponge).  So, to a certain extent, this is “cheating” – if there’s a non-trivial amount of data, sponge almost certainly uses a temporary file.  But it’s invisible to you; you don’t have to worry about housekeeping things like choosing a unique filename and cleaning up afterwards.

The { IFS= read -r x; { printf "%s\n" "$x"; cat; } | commandB; } reads the first line of output from sponge.  Remember, this doesn’t appear until commandA has finished.  Then it fires up commandB, writes the first line to the pipe, and invokes cat to read the rest of the output and write it to the pipe.

  • thanks! So sponge does that thing I used mbuffer for, but seems to be better suited here. using read is clever here. Definitely will remember this for the future. – Josef Mar 5 '15 at 8:33
  • @Josef: I've never heard of mbuffer before; it might actually be just as good as sponge. I agree that using read is a clever trick. I can't take full credit for it; it pops up in answers throughout Stack Exchange (U&L, Super User, Ask Ubuntu, etc.) from time to time. In fact, roaima's answer to this question is very similar to mine except it doesn't use sponge (or something equivalent), so, as I mentioned in a comment, it doesn't delay starting commandB as much as you need (in my understanding of your problem). – G-Man Mar 5 '15 at 18:24
4

Commands in pipe line are started concurrently, you need to store commandA output somewhere to use later. You can avoid temp file by using variable:

output=$(command A; echo A)
printf '%s' "${output%%A}" | commandB
  • What is the purpose of the echo A in the process substitution? – Digital Trauma Mar 4 '15 at 18:09
  • 3
    @DigitalTrauma: It prevents command substitution from stripping trailing newlines. – cuonglm Mar 4 '15 at 18:10
  • Ah, I see, yes - good catch of a possible corner case – Digital Trauma Mar 4 '15 at 18:13
  • I realized my answer was incorrect, so I deleted it. I think this looks correct instead. +1 – Digital Trauma Mar 4 '15 at 18:26
  • Thanks a lot! Thats great and especially the echo A/%%A to keep the newline (even if I don't require that) – Josef Mar 5 '15 at 8:31
1

I don't know of any standard UNIX utility that can address this issue. One option would be use awk to accumulate commandA output and flush it to commandB at one shot, like so

commandA  | awk '{x = x ORS $0}; END{printf "%s", x | "commandB"}'

Beware that this could be memory intensive since awk is building up a string from its input.

  • 1
    This strips off the last newline. Assuming that the last character is a newline, you need a \n or another ORS at the end. – G-Man Mar 4 '15 at 20:32
0

You can solve the requirement with a little script. This particular variant avoids the temporary file and potential memory hog at the expense of additional processes.

#!/bin/bash
#
IFS= read LINE

if test -n "$LINE"
then
    test -t 2 && echo "Starting $*" >&2
    (
        echo "$LINE"
        cat

    ) | "$@"
else
    exit 0
fi

If you were to call the script waituntil (and make it executable, put it in the PATH, etc), you'd use it like this

commandA... | waituntil commandB...

Example

( sleep 3 ; date ; id ) | waituntil nl
  • 1
    All this does is delay starting commandB until commandA has written its first line of output. That probably isn't good enough. – G-Man Mar 4 '15 at 20:35
  • @G-Man but we don't know. I like your sponge. Off to read up on that one now. – roaima Mar 4 '15 at 21:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.