2

I am writing an interactive shell script that needs to run on as many systems as possible. Is there an alternative way to implement the following that is compatible with a standard POSIX system?

#! /bin/bash
echo -n "Do you wish to continue? (Y/n) 5 seconds to respond... "
read -n 1 -t 5 answer # accepts a single character, 5 second timeout.
if [ "$answer" = "n" ] || [ "$answer" = "N" ] ; then
  echo -e "\nExiting..."
  exit
fi
echo -e "\nContinuing with script..."
# More code

The timeout on read is most important to me (read -t 5). The one character limit for read is desirable but not essential (read -n 1). Ideally, the script would work on POSIX systems and also within bash without having to enable a special POSIX compatibility mode.

HullCityFan852
  • 212
  • 1
  • 11

2 Answers2

4

Adapting the stty settings from Mr Dickey's answer, the following seems to work and continues with the script if anything other than 'n' or 'N' is pressed.

As far as I can tell, all of the stty settings are posix.

#!/bin/sh

read_char() {
        old=$(stty -g)
        stty raw -echo min 0 time 50
        eval "$1=\$(dd bs=1 count=1 2>/dev/null)"
        stty $old
}

printf "Do you wish to continue? (Y/n) 5 seconds to respond..."
read_char answer
# answer=$(getch)
# answer=$(getche)

if [ "$answer" = "n" ] || [ "$answer" = "N" ] ; then
  printf "\nExiting...\n"
  exit
fi

printf "\nContinuing with script...\n"

Alternatives to "read_char":

This avoids using eval (can be unsafe)

getch() {
        old=$(stty -g)
        stty raw -echo min 0 time 50
        printf '%s' $(dd bs=1 count=1 2>/dev/null)
        stty $old
}

This avoids eval and prints the pressed key

getche() {
        old=$(stty -g)
        stty raw min 0 time 50
        printf '%s' $(dd bs=1 count=1 2>/dev/null)
        stty $old
}
HullCityFan852
  • 212
  • 1
  • 11
user464502
  • 2,203
  • 11
  • 14
  • Using eval on unpredictable user input and without checking $1 is a potentially dangerous idea. I think you need to only output 1 character as a function return, i.e. it will be better to `dd bs=1 count=1 2>/dev/null`, but not `eval "$1=\$(dd bs=1 count=1 2>/dev/null)"`. – drvtiny Aug 28 '15 at 11:53
  • I agree, I was just cribbing from another post and trying to match the functionality of read as closely as possible. If I were going to use this code myself, I would have read_char just print out the character and call it as `answer=$(read_char)` However, there is no need to check $1 since the script specifies it. And the unpredictable user input is guaranteed to be a single character. – user464502 Aug 28 '15 at 11:56
  • BASH is very buggy, so, for example, if "user" input byte with value 0 - it may or may not work, because you cant test this in all versions of Shell. – drvtiny Aug 28 '15 at 13:06
  • @user464502 I really like this answer and your suggestion to make it case-insensitive. I've suggested an edit that implements this and removes the eval statement since it is probably best avoided (though it is harmless here, as you say). The pressed key is also displayed. – HullCityFan852 Aug 31 '15 at 14:19
  • Apparently echo -n isn't posix. I changed that to a printf. Obviously each user would need to adapt the script to their particular use. Echoing the character pressed can be done by removing the -echo from the stty line. I have added a couple of functions illustrating the answer=$(function) style. Personally, I would probably have those functions take the prompt as arguments and do a `printf '%s' $*` or similar, but that's a trivial change. – user464502 Aug 31 '15 at 16:39
1

The stty program provides the means to do this. xterm has several scripts (in its source's "vttests" subdirectory) which save, modify and restore terminal settings to allow it to read the terminal's response to escape sequences. Here is a part of dynamic2.sh (the beginning sets up printf or echo, to address some old systems with the $CMD variable):

echo "reading current color settings"

exec </dev/tty
old=`stty -g`
stty raw -echo min 0  time 5

original=
for N in $FULL
do
    $CMD $OPT "${ESC}]$N;?^G${SUF}" > /dev/tty
    read reply
    eval original$N='${reply}${SUF}'
    original=${original}${reply}${SUF}
done
stty $old

if ( trap "echo exit" EXIT 2>/dev/null ) >/dev/null
then
    trap '$CMD $OPT "$original" >/dev/tty; exit' EXIT HUP INT TRAP TERM
else
    trap '$CMD $OPT "$original" >/dev/tty; exit' 0    1   2   5    15
fi
Thomas Dickey
  • 51,086
  • 7
  • 70
  • 105