Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using read -p in a bash script that was executed from pipe

I apologize in advance - I don't fully understand the ideas behind what I'm asking well enough to understand why it's not working (I don't know what I need to learn). I searched stack exchange for answers first - I found some information that seemed possibly relevant, but didn't explain the concepts well enough that I understood how to build a working solution. I've been scouring google but haven't found any information that describes exactly what's going on in such a way that I understand. Any direction to background concepts that may help me understand what's going on would be greatly appreciated.

Is it possible to get user input in a bash script that was executed from a pipe?

For example:

wget -q -O - http://myscript.sh | bash

And in the script:

read -p "Do some action (y/n): " __response
if [[ "$__response" =~ ^[Yy]$ ]]; then
   echo "Performing some action ..."
fi

As I understand it, this doesn't work because read attempts to read the input from stdin and the bash script is currently "executing through that pipe" (i'm sure there is a more technical accurate way to describe what is occurring, but i don't know how).

I found a solution that recommended using:

read -t 1 __response </dev/tty

However, this does not work either.

Any light shed on the concepts I need to understand to make this work, or explanations of why it is not working or solutions would be greatly appreciated.

like image 652
nfarrar Avatar asked Apr 12 '14 17:04

nfarrar


People also ask

What is the use of read command in a bash script?

Bash read Syntax The read command takes the user input and splits the string into fields, assigning each new word to an argument. If there are fewer variables than words, read stores the remaining terms into the final variable. Specifying the argument names is optional.

How do you read a pipe?

If one end of the pipe is closed, 0 is returned, indicating the end of the file. If the write side of the FIFO has closed, read(2) returns 0 to indicate the end of the file. If some process has the FIFO open for writing, or both ends of the pipe are open, and O_NDELAY is set, read(2) returns 0.

How do I read a text file line by line in bash?

We can use the read command to read the contents of a file line by line. We use the -r argument to the read command to avoid any backslash-escaped characters. In the following example, we can see that we have an iteration over a file line by line and we store the contents of a single line in the variable “line“.

What is $@ in bash?

bash [filename] runs the commands saved in a file. $@ refers to all of a shell script's command-line arguments. $1 , $2 , etc., refer to the first command-line argument, the second command-line argument, etc.


2 Answers

The tty solution works. Test it with this code, for example:

$ date | { read -p "Echo date? " r </dev/tty ; [ "$r" = "y" ] && cat || echo OK ; }
Echo date? y
Sat Apr 12 10:51:16 PDT 2014
$ date | { read -p "Echo date? " r </dev/tty ; [ "$r" = "y" ] && cat || echo OK ; }
Echo date? n
OK

The prompt from read appears on the terminal and read waits for a response before deciding to echo the date or not.

What I wrote above differs from the line below in two key aspects:

read -t 1 __response </dev/tty

First, the option -t 1 gives read a timeout of one second. Secondly, this command does not provide a prompt. The combination of these two probably means that, even though read was briefly asking for input, you didn't know it.

like image 125
John1024 Avatar answered Oct 14 '22 08:10

John1024


The main reason why this is not working is, as the OP validly indicated,

  • The | <pipe> which is used, sends the standard output from the first command as standard input to the second command. In this case, the first command is

    wget -q -O - http://myscript.sh
    

    which passes a downloaded script via the pipe to its interpreter bash

  • The read statement in the script uses the same standard input to obtain its value.

So this is where it collapses because read is not awaiting input from you but takes it from its own script. Example:

$ cat - <<EOF | bash 
> set -x
> read p
> somecommand
> echo \$p
> EOF
+ read p
+ echo somecommand
somecommand

In this example, I used a here-document which is piped to bash. The script enables debugging using set -x to show what is happening. As you see, somecommand is never executed but actually read by read and stored in the variable p which is then outputted by echo (note, the $ has been escaped to avoid the substitution in the here-document).

So how can we get this to work then?

First of, never pipe to an interpreter such as {ba,k,z,c,tc,}sh. It is ugly and should be avoided, even though it feels the natural thing to do. The better thing to do is to use any of its options:

bash -c string: If the -c option is present, then commands are read from string. If there are arguments after the string, they are assigned to the positional parameters, starting with $0.

$ bash -c "$(command you want to pipe)"

This also works for zsh, csh, tcsh, ksh, sh and probably a lot of others.

like image 25
kvantour Avatar answered Oct 14 '22 06:10

kvantour