3

Is there a way to make my script wait for the user to press any key and then continue without them having to press Enter? I want to make it work in Bourne shell (sh), not Bash.

1 Answers1

2

Preliminary notes

Nowadays sh is not necessarily the legacy Bourne shell. Nowadays sh is a shell that supports at least the features required by POSIX (assuming a correct implementation). POSIX specifies the sh utility and the Shell Command Language.

I assume you want your code to work in a POSIX shell and to be portable in general.


Issues

dd ibs=1 count=… is a POSIX way to read an exact number of bytes. It seems to be the only portable command line utility that can do the job reliably. But dd reads bytes, while read -n in Bash reads characters. POSIX allows multi-byte characters in locales other than POSIX. Even if you run the entire script with LC_ALL=POSIX, the terminal (terminal emulator) may still generate a multi-byte sequence upon some single keystroke. Such sequence may not be a multi-byte character; it may be an escape sequence (e.g. for F1, also see this answer).

After you read just one byte, the rest will remain and it will be read by anything that tries to read from the terminal later (it may be a part of your script or the interactive shell you run the script from).

Additionally if you run sole dd ibs=1 count=1 then you will most likely discover that nothing gets to dd until you exceed {MAX_INPUT} or {MAX_CANON}, or press Enter or Ctrl+D (and in case of count being more than 1, if you want to provide less bytes then you may need to press Ctrl+D twice).

To deal with these issues you need the non-canonical mode. Use stty -icanon. In general starting with stty raw looks like a good idea.


Code

The following example is a proof of concept. Because it manipulates line settings of the terminal and reads from the terminal you must not paste it into an interactive shell, this won't work. Paste it into a file and run the file.

#!/bin/sh

echo "Press any key to continue."

saveterm="$(stty -g)" # save terminal state stty raw stty -echo -icanon min 1 time 0 # prepare to read one byte dd ibs=1 count=1 >/dev/null 2>/dev/null # read one byte stty -icanon min 0 time 0 # prepare to read lefotvers while read none; do :; done # read leftovers stty "$saveterm" # restore terminal state


Notes

  • The script doesn't check if its stdin is a terminal, but in general it should ([ -t 0 ]).

  • In a sane setup a modifier key (e.g. Shift), when pressed alone, sends no input to whatever reads from the terminal; therefore our code will not register such key as "any key".

  • The "read leftovers" trick is taken from this answer.

  • Thanks to stty raw Ctrl+C or Ctrl+Z can be "any key" as well, instead of sending SIGINT or SIGSTOP respectively. Still the shell interpreting the script may get a signal from elsewhere, so in general it may happen it exits before it gets to stty "$saveterm"; so it may happen it leaves the terminal in a state not suitable for interactive use. You may want to trap relevant signals and restore the initial state of the terminal anyway.

  • It's true one should double-quote variables in general. Here $saveterm is deliberately not quoted because stty -g generates output that is unspecified. Implementations of stty are allowed to generate output with spaces and later expect the shell to split it and pass multiple arguments. What is specified is a constraint that the output of stty -g must be safe when unquoted, it must not trigger word expansion in a shell. For portability unquoted $saveterm is better.

    Edit: that was a defect in the POSIX specification. The above code has been fixed.

  • To know what byte was read you need to save the output of dd, either to a regular file (>some_file) that you will examine later, or to a variable (variable="$(dd …)"). But:

    • A terminal is able to generate a null byte (commonly with Ctrl+@) but most (all?) implementations of sh cannot store a null byte in a variable. Storing in a file is OK, if only you can examine/manipulate its content without reading it into a variable.
    • dd from the above code will give you only one byte, it may not be enough to tell the character or the key. OTOH a single byte should be enough to tell apart e.g. Q from anything else, so Press Q to quit or any other key to continue. or P - proceed; B - back; Q - quit; H - help seem possible to implement without analyzing more bytes.

What about more bytes?

The above should be fine for "press any key". A real equivalent of read -n should read one byte at a time and decode the sequence until it gets the desired number of characters. I'm not going to try to build it here.