2

I often execute raw versions of remote Bash scripts in GitHub with this pattern:

wget -O - https://raw.githubusercontent.com/<username>/<project>/<branch>/<path>/<file> | bash

Generally I can do it without problems but since I have added the following code to a certain script I get an endless loop of echo (it often occurs even if I just copy-paste it from GitHub to the terminal and execute directly):

while true; do
    read -p "Example question: Do you wish to edit the PHP file now?" yn
    case $yn in
        [Yy]* ) nano PROJECT/PHP_FILE; break;;
        [Nn]* ) break;;
        * ) echo "Please answer yes or no.";;
    esac
done

I can at least partly solve the problem with something like:

cd ACTION_DIRECTORY
wget https://raw.githubusercontent.com/<username>/<project>/<branch>/<path>/<file> &&
source FILENAME &&
rm FILENAME

Which indicates that the | bash piping at least worsens the problem because the problem always happens with it.

Why would echo "Please answer yes or no." happen "endlessly"? (which I stop it with CTRLC+C)
Do you find any problem either in the single lined execution command and/or in the while true; do case esac done?

1 Answers1

5

Why would echo … happen "endlessly"?

With wget … | bash you pipe wget to bash. The stdin of bash comes from wget. read reads from the same stdin.

In general read reading from where the script comes from can consume parts of the script. In your case bash needs to read the whole while … done fragment (because e.g. it could be while … done <whatever). When read works, there is nothing more to read. Even the first read fails.

The script in question doesn't check if read fails.

Additionally read -p prints its prompt to stdin, so you never see the prompt.

If in the script there was </dev/tty read … or while … done </dev/tty then read would not read from the stdin of bash, it would read from the console. This would work but the method requires a change in the script itself. In general the change would break things if you run the script differently and need read to read from the stdin of the entire script that would happen to be different than /dev/tty.

But then nano (if you answered y) may complain about its stdin. If your fix was </dev/tty read … then nano would yield Too many errors from stdin because its stdin would be the (already broken) pipe from wget. If your fix was while … done </dev/tty then it would work.

With a bigger script there may be more places to "fix" this way. A proper general solution is not to hijack the stdin at all.

A general solution is to run one of these:

bash <(wget -O - …)
.    <(wget -O - …)

depending on if you want the script to run in a separate bash (like in your wget … | bash … try) or in the current shell (like in your source FILENAME solution).

Note you can now redirect the stdin independently, e.g.:

yes | bash <(wget -O - …)

The <(some_command …) syntax is called process substitution. It works in Bash and in few other shells but not in pure sh (see bashisms). Some interesting observations here: Process substitution and pipe (the "Preserving STDIN" section of this answer is exactly your question in a nutshell).

  • Thank you. I, as someone which is not a professional sysadmin and don't maintain servers daily, prefer not to go deep with the syntaxes that you have presented and stay with a more simple/traditional (non sugary-syntax) approach. Do you think that the alternative code I myself presented with source is good? Would you change anything there to make it more friendly with the loop and case esac? Thanks anyway, – variable_expander Mar 20 '21 at 11:45
  • I have posted a new question on the subject, you might want to check it: https://unix.stackexchange.com/questions/640206/understanding-the-process-substitution-concept – variable_expander Mar 20 '21 at 12:13
  • @variable_expander Your code with source realizes a good idea (download first, run later) with some potential flaws: (1) unquoted ${HOME}; (2) && after source … makes rm depend on what the script does (this may or may not be what you want). // In general it's better to run a script (i.e. not to source), unless you know you need to source. bash FILENAME runs it by a separate bash. ./FILENAME runs it according to the shebang (if present), but FILENAME needs to be executable. – Kamil Maciorowski Mar 20 '21 at 12:36