2

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...

  • 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 Apr 13 '18 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 Apr 13 '18 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 Apr 13 '18 at 18:44
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.
  • 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 Apr 13 '18 at 15:50
  • 1
    I know, but as presented cat file | say will say the contents of the file. That's the problem. – DopeGhoti Apr 13 '18 at 15:51
  • Even when say is given a command line argument? – Kusalananda Apr 13 '18 at 15:52
  • In that case, the argument is ignored, making the useless use of cat even more useless (: – DopeGhoti Apr 13 '18 at 15:54
  • 1
    @DopeGhoti I can't reproduce what you said above. echo hello | say "goodbye" says "goodbye". – Kusalananda Apr 13 '18 at 16:57
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"
  • 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 Apr 13 '18 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 Apr 14 '18 at 0:30
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 ; }
  • This isn't quite right, since cat doesn't start until after the word "one" is spoken. – agc Apr 13 '18 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 -
  • 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 Jan 25 '19 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
    There is no output from say to pipe. – Kusalananda Apr 13 '18 at 15:47

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.