6

I'm trying to figure out how to wait for a command to complete, and then pipe stdin to stdout. I'm on a mac, but I think my question is more about how to wait for a process to complete and pipe the output then anything to do with a mac.

I've noticed that on the mac, I can run a few say commands together, and they wait for each phrase to be fully spoken before starting the next one, if I use the && operator to join them up.

$ say "stage 1" && say "stage 2"

Here's where the real use case is - I've got a bash script that I'd like to have it just pass stdin to stdout, after it finishes saying something.

$ cat /etc/passwd | say_and_pass "stage 1" | grep -v test | say_and_pass "stage 2"

So conceptually, this would say aloud, "stage 1", then immediately say "stage 2", and then dump the grepped contents of /etc/password to stdout.

My initial crack at the say_and_pass script is this:

say_and_pass

#!/usr/bin/env bash
OUT="$*"
say "$OUT" && cat 

But it doesn't seem to work ;-)

EDIT: I changed the example above to use say_and_pass "stage2" as the final command, which is needed for my solution to work...

3
  • Please tell us in what way this does not work. If there's an error message, please include this in full. Your code works for me on an up-to-date macOS machine, but I don't know if it's working in the way that you expect.
    – Kusalananda
    Commented Apr 13, 2018 at 17:46
  • 1
    Thanks for the feedback - I'm using HIgh Sierra, and when I run the above command that uses say_and_pass, it says both "stage 1" and "stage 2" at the same time, instantly. I'd like it to say "stage 1", then immediately say "stage 2" afterwards. The intent of this is for me to be able to put audio alerts in the middle of a series of pipes. That way I'll know when some stages have completed. This is of course for long running processes that I don't want to sit and have to watch to see if they're done. The thing is sometimes, the scripts will finish fast, and talk over top of each other.
    – Brad Parks
    Commented Apr 13, 2018 at 18:23
  • 1
    In testing on my Mac here, say blocks until it is done outputting the audio; it should not overlap itself unless you are running two instances of say simultaneously. That said, your I/O stream is broken in your example here, so with pipes you essentially are running two instances of 'say' near-parallel. Compare echo "foo" 1>&2 | say "foo" | echo "bar" 1>&2 | say "bar" to echo "foo" 1>&2 ; say "foo" ; echo "bar" 1>&2 ; say "bar".
    – DopeGhoti
    Commented Apr 13, 2018 at 18:44

6 Answers 6

1

Here's where the real use case is - I've got a bash script that I'd like to have it just pass stdin to stdout, after it finishes saying something.

$ cat /etc/password | say_and_pass "stage 1" | grep -v test | say "stage 2"

The problem with what you're trying to do here is that you seem to have two data streams- one for say and one for text processing (i. e. cat and grep).

You can't use a single pipeline for this, as it will just commingle the data. cat /etc/passwd | say will have your computer attempt to speak the entire contents of the file. Furthermore, say does not write anything to standard output, so nothing would go any farther down the pipeline.

If you do want to "interrupt" your data processing stream to handle separate output to other utilities like say, you need to either set yourself up with a FIFO (i. e. a "named pipe") for one or the other of your data streams, or use temporary files written to disk.

Your demonstrative use-case isn't very helpful for an example, as notwithstanding the says, you're just grepping a file, which can be done in one step with grep -v test /etc/passwd (cat file | grep pattern is a useless use of cat).

All that said, an example using a scratch file:

scratch=$(mktemp)
trap "rm -f $scratch" EXIT
cat /etc/passwd > $scratch
say "Stage one"
grep -v test $scratch
say "Stage two"

And one used a named pipe for say:

mkfifo youtalktoomuch
say youtalktoomuch &
exec 3> youtalktoomuch
do_thing_one
echo "Stage one" > youtalktoomuch
do_thing_two
echo "Stage two" > youtalktoomuch
exec 3>&-
rm youtalktoomuch

Once again, though, if you need to connect the output of `do_thing_one` to the input of `do_thing_two` without piping one into the other, you will need to use either another named pipe or a scratch file on disk to hold the data.
8
  • 1
    He's not trying to get say to say the contents of the file, as far as I can tell, just "stage 1" and "stage 2".
    – Kusalananda
    Commented Apr 13, 2018 at 15:50
  • 1
    I know, but as presented cat file | say will say the contents of the file. That's the problem.
    – DopeGhoti
    Commented Apr 13, 2018 at 15:51
  • Even when say is given a command line argument?
    – Kusalananda
    Commented Apr 13, 2018 at 15:52
  • In that case, the argument is ignored, making the useless use of cat even more useless (:
    – DopeGhoti
    Commented Apr 13, 2018 at 15:54
  • 1
    @DopeGhoti I can't reproduce what you said above. echo hello | say "goodbye" says "goodbye".
    – Kusalananda
    Commented Apr 13, 2018 at 16:57
0

This version of say_and_pass does what OP asks for:

#!/usr/bin/env bash

# Create a temporary file to store stdin
TEMP_STDIN="$(mktemp)"

# Capture stdin and save it to temporary file
cat - > "${TEMP_STDIN}"

# say message, but prevent any output to stdout
say "$@" > /dev/null

# Dump saved stdin to stdout
cat "${TEMP_STDIN}"

# Clean up, but prevent any output to stdout
rm -f "${TEMP_STDIN}" > /dev/null

It uses a temporary file, like @DopeGhoti suggests, but keeps any of the other logic (the grep, for instance) out of the script. This makes it so that say_and_pass is more general and reusable.

My interpretation of the goal/purpose is that we want an audible way to monitor progress of a potentially long pipeline.

So when running:

cmd1 | say_and_pass "stage 1" | cmd2 | say_and_pass "stage 2"

As a user, you can audibly know that cmd1 has fully completed its execution; the trade-off is that cmd2 cannot begin processing its stdin until cmd1 is complete. But it seems that the OP is aware of this trade-off and willing to make it.

This is similar to the answer by @BradParks, but is not limited by the memory available to bash (as @agc points out), and is slightly more careful about avoiding accidental pollution of stdout.

Also note that there's no need to capture the message into a variable of its own, so we also simply forward "$@" to say.

4
  • 1
    There's already an accepted answer that resolves the issue. Also DopeGhoti's answer resolves it. Have you tested your solution so you can confirm it works? If yes, could you please expand the answer to explain what exactly does this add to DopeGhoti's answer? If you haven't tested and can't confirm I'd suggest removing the answer. Commented Sep 29, 2021 at 7:06
  • If another answer is missing bits, you should suggest a modification to that answer. Each answer needs to be self-contained.
    – Kusalananda
    Commented Sep 29, 2021 at 7:57
  • This does not provide an answer to the question. Once you have sufficient reputation you will be able to comment on any post; instead, provide answers that don't require clarification from the asker. - From Review
    – Kusalananda
    Commented Sep 29, 2021 at 7:57
  • I'm the OP, and have tested this solution on my mac, and it does exactly what I want, and is a better version of my answer! Thanks @walter
    – Brad Parks
    Commented Sep 30, 2021 at 9:46
0

Turns out all I had to do was consume all of stdin at the start of my script... I don't want it to process stdin as it's received, but when the entire previous command in the pipe chain completes.

So my say_and_pass script is simply this, which works for my use case!

#!/usr/bin/env bash

OUT=$(cat -)

MSG="$*"
say "$MSG"

echo "$OUT"
6
  • It may work for the use case, but it's not general. For example it won't work for files larger than bash's available memory.
    – agc
    Commented Apr 13, 2018 at 20:10
  • @agc - now that I've given it more thought, there's no way it can work any other way... normally piping passes content through the entire pipe line every time a line of input is received.... which is why my previous attempts didnt work. Any other approach doesnt make sense...
    – Brad Parks
    Commented Apr 14, 2018 at 0:30
  • Consider using a temporary file instead of a variable, like DopeGhoti. Also, you might want to protect against unexpected stdout coming from the "say" command: say "$MSG" > /dev/null
    – Walter
    Commented Sep 29, 2021 at 19:39
  • @Walter - can you add that as another answer, and I'll use it then as the accepted, best answer
    – Brad Parks
    Commented Sep 29, 2021 at 20:45
  • 1
    Thanks @BradParks! I undeleted and updated my answer.
    – Walter
    Commented Sep 30, 2021 at 3:31
0

Try:

say "stage 1" && cat /etc/passwd  |  { say "stage 2" && grep -v test ; }

I don't actually have say on my system, but this works:

espeak "stage 1" && cat /etc/passwd  |  { espeak "stage 2" && grep -v test ; }
1
  • This isn't quite right, since cat doesn't start until after the word "one" is spoken.
    – agc
    Commented Apr 13, 2018 at 20:01
0

H!

I'd suggest, based on your own answer, to just dump stdin to stdout, removing the part which stores it to a variable. This should reduce memory consumption, and make the pipe chain work as pipes should (one line in, it gets processed, and the result sent to the next process).

#!/usr/bin/env bash

MSG="$*"
say "$MSG"

cat -
1
  • Thanks for the suggestion! I tried it and it seems that both "says" happen at the same time using this.... I think doing it in a subshell forces the full stdin to be consumed.... try out the example I have... does it work for you? ie $ cat /etc/passwd | say_and_pass "stage 1" | grep -v test | say_and_pass "stage 2"
    – Brad Parks
    Commented Jan 25, 2019 at 15:10
-3
#!/usr/bin/env bash
OUT="$*"
say "$OUT" | cat -

If you use &&, its meaning , do fisrt command after do second command but fisrt command result not important for second command. I mean you have 2 different command, because you use &&. If you use |, its means fisrt command do something and give something for second command. The end, you should give a attribute for cat command. Sorry my english by the way

1
  • 1
    There is no output from say to pipe.
    – Kusalananda
    Commented Apr 13, 2018 at 15:47

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .