3

I'm having quite some trouble with two bash-scripts I am working on:

script.sh

#!/bin/bash
while true; do
    read -p "script: read: var: " var
    echo "script: write: var: $var"
done

pipe.sh

#!/bin/bash
while read line; do
        echo "pipe: read: line: (( $line ))"
        read -p "pipe: read var: " var < /dev/tty
        echo "pipe: write: var: $var"
done< <(cat) 

When executing script.sh and piping the output into pipe.sh i get following output:

$ ./script.sh | ./pipe.sh
1: script: read: var: 123   # user entering '123'
2: script: read: var: pipe: read: line: (( script: write: var: 123 ))
3: pipe: read var: 321      # user entering '321'
4: script: read: var: 456   # user entering '456'
5: pipe: write: var: 456
6: pipe: read: line: (( script: write: var: 321 ))
7: pipe: read var: 

As you can see everything seems to work until getting to line 4. I was expecting line 4 to be pipe: write: var: 321 from pipe.sh. Instead I get the prompt from script.sh.

Upon entering the string "456" the previously expected line is executed, but with the wrong string (expected: "321", got "456"). In addition line 6 does not print "456" but "321" instead.

Something is totally wrong here. Any suggestions on how to fix this and why this is happening?

Update:

Essentially i would like the pipe to work the same way as the code below.

script1.sh

#!/bin/bash
while true; do
  read -p "val1: " val1
  script2.sh "${val1}"
done

script2.sh

#!/bin/bash
val1="${1}"
read -p "val2: " val2
echo "${val1} ${val2}"

However, i do not want to hard-code script2.sh into script1.sh. I could pass script2.sh as argument to script1.sh but i initially thought a pipe would be a better solution?

6
  • 2
    The read -p calls in both script.sh and pipe.sh read from the current terminal, and since the commands in a pipeline run in parallel, you can make no assumptions about which of them is first to snatch the data entered by the user. The read -p from one script.sh may put out its prompt, but the string entered by the user may be read by the read -p from pipe.sh and vice-versa. I suggest that you re-think your approach.
    – user313992
    Commented Apr 10, 2020 at 21:22
  • You cannot have multiple programs/scripts interacting with user in the same terminal at the same time.
    – user313992
    Commented Apr 10, 2020 at 21:29
  • if i understand correctly the only solution would be to not use a pipe e.g. passing the script as parameter e.g. ./script.sh pipe.sh instead? Commented Apr 10, 2020 at 21:41
  • The solution depends on what you're actually trying to do. Just calling ./script.sh pipe.sh with the those scripts will not run pipe.sh at all ;-)
    – user313992
    Commented Apr 10, 2020 at 22:13
  • I want to execute everything in order. When script.sh is executed and prints the first line (e.g. test1) it should be piped to pipe.sh. pipe.sh should process test1 and wait for the next line. when pipe.sh has processed test1 the next line (e.g. test2) should be piped to pipe.sh and so on. Commented Apr 10, 2020 at 22:28

1 Answer 1

2

The read -p calls in both script.sh and pipe.sh read from the current terminal, and since the commands in a pipeline run in parallel, you can make no assumptions about which of them is first to snatch the data entered by the user.

The read -p from one script.sh may put out its prompt, but the string entered by the user may be read by the read -p from pipe.sh and vice-versa.

In a pipeline like a | b, b could be easily made to wait for input from a before proceeding further, but the reverse is not true: since pipes are buffering, a would have to write a lot of data before noticing that b is not reading any of it.

One way out of this is to connect the stdout of b with the stdin of a in a kind of "circular pipeline", and modify a (script.sh) to also wait for input from stdin, just like b (pipe.sh) does.

Because of the shell language's limitations, you should use a named pipe for that. Simple example:

cat > circpipe <<'EOT'; chmod 755 circpipe
fifo=$(mktemp -u)
mkfifo "$fifo" || exit 1
exec 3<>"$fifo" >"$fifo" <"$fifo"
rm "$fifo"
echo trigger
"$1" | "$2"
EOT

cat > pipe-head <<'EOT'; chmod 755 pipe-head
while read next; do
        read -p "HEAD's prompt>> " var </dev/tty || exit
        echo "$var"
done
EOT

cat > pipe-tail <<'EOT'; chmod 755 pipe-tail
while read input; do
        echo >&2 "  TAIL'input: $input"
        read -p "  TAIL's prompt>> " var </dev/tty
        echo >&2 "  TAIL processing <$var>"
        echo next       # trigger the head of the pipeline
done
EOT
./circpipe ./pipe-head ./pipe-tail
HEAD's prompt>> foo
  TAIL'input: foo
  TAIL's prompt>> bar
  TAIL processing <bar>
HEAD's prompt>> baz
  TAIL'input: baz
  TAIL's prompt>> quux
  TAIL processing <quux>
HEAD's prompt>> ^D$

The circpipe script could be made into a more general tool, which would accept regular shell commands, and where its "tail" could also break out of the loop.

Unlike the example above, this will not "kick start" the loop by default; for that, a -command argument should be used. Example use:

./circpipe -echo './pipe-head | stdbuf -oL sed s/e/o/g | ./pipe-tail'
HEAD's prompt>> pee
  TAIL'input: poo
  TAIL's prompt>> lol
  TAIL processing <lol>
HEAD's prompt>>^D

circpipe

#! /bin/sh
# usage: circpipe [-start_command] shell_command
# run 'shell_command' with its stdin connected to its stdout
# via a FIFO
# if 'start_command' is given, run it before `shell_command`
# with its stdout redirected to the same FIFO
[ "$#" -gt 0 ] || exit 0
fifo=$(mktemp -u)
mkfifo "$fifo" || exit 1
exec 3<>"$fifo" >"$fifo" 3<"$fifo"
rm "$fifo"
case $1 in -*)  eval "${1#-}"; shift; esac
IFS='|'; eval "<&3 $* &"
exec >&-
wait
3
  • thanks, quite impressive answer. However, i would prefer something like ./circlepipe | ./pipe-head | cat | ./pipe-tail or even better ./circlepipe-head | ./pipe-tail. Is this possible? Commented Apr 11, 2020 at 2:54
  • I have added a hopefully less bamboozling explanation, and an example which could be useful for me too ;-)
    – user313992
    Commented Apr 11, 2020 at 8:42
  • Thanks for your updated answer! While (unfortunately) the script itself is not quite what i'm looking for the explanation why my approach didn't and won't work answers my question. Maybe i need to take the approach of @Kusalananda (which i was quite aware of how to do this in the first place). However, i'm quite surprised that there is no "off-the-shelf" solution to this kind of problem. Commented Apr 11, 2020 at 13:50

You must log in to answer this question.

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