------------------------------------------------------------------------------- Input Reading Hints and Tips on reading user input from Shell Scripts Including... * general reading * input timeouts * providing input history * input helper programs * binary streams ------------------------------------------------------------------------------- Multiple choice ('select') Bash has a multple choice command 'selete'... =======8<-------- PS3_save="$PS3" PS3="Select> " select choice in First "Second Choice" ""; do case $choice in '') echo "Invalid choice, try again..." ;; *) echo "You Selected: $name"; break ;; esac done PS3="$PS_save" =======8<-------- User should select a number and $name will be set to the value selected. Valid Numbers include: 01 +1 but not 1.0 Otherwise $choice will be the empty string, when... * User did not type a number -- This inlucdes when they typed the value! * number is out of bounds * the actual value for that number is blank. If the $choice comes out blank, you can use $REPLY, which will contain the actual response from the user. (like a normal 'read') NOTE: If a 'break' is not given in the 'select' loop, selete will loop and the whole menu will then be re-typed. This could result in a lot of terminal scrolling! Using alt screen may be useful here. Multiple selections is not posible using 'select'. While you can get users to make multiple selections, you cannot show the selections the user has made. Doing this will make using 'select' very complicated. For anything more than a simple choice, you may be better off using one of the more complex input tools (if one is available). (see "input_tools.txt" ) ------------------------------------------------------------------------------- Fully typed 'Yes' or 'No' question Just like ssh hostkey check does... yesno() { local prompt="$* (yes/no): " while true; do read -r -p "$prompt" answer case "$answer" in [yY][eE][sS]) return 0 ;; [nN][oO]) return 1 ;; *) prompt="Please type 'yes' or 'no': " ;; esac done } if yesno "Do you want to 'clear' your working dir?"; then echo "Clearing working directory" # ... DO IT ... else echo "Aborting working directory clear" fi For a WHOLE list of methods of handling Yes/No... https://stackoverflow.com/questions/226703/a/27875395#27875395 This includes method for... * single key input, (using "read", "stty", etc) * multiple languages * input tools (like "dialog", "whiptail", see "input_tools.txt") * with history! =============================================================================== Reading a line of input This is not always as easy as it seems.... while read -r line || [ -n "$line" ]; do echo "$line" done The "read -r" will ensure it does not try to interpret backslashes. Specifically a backslash at the end of the line that will make it read the next line as part of the same input. The "[ -n "$line" ]" is also important when reading a file that does not end in a newline. This ensures the while loop will process a incomplete last line, but not a blank last line (as file did end in a newline). It will add the newline on the echo output!" An alternative is to process the "[ -n "$line" ]" after the while loop ends so you can deal with the final (no newline) line separately, and not output the newline! while read -r line; do echo "$line" done [ -n "$line" ] && echo -n "$line" ------------------------------------------------------------------------------- Just read values from first line of a command # get count and directory with highest inode count (junk the rest) read count directory < <(du -xs --inodes /app/docker/overlay2/* | sort -nr ) ------------------------------------------------------------------------------- Read a Line with a Timeout (non-BASH) WARNING: this really only works from a script! Do not cut and paste to command line to try this Using Stty (fails) # read with 4 second timeout old_tty_settings=`stty -g` stty min 0 time 40 read input stty "$old_tty_settings" echo $input Note the stty timer is reset after every keystoke! It is not a total timeout. Timed version (fails) : #Dan Mercer: damercer@mmm.com wake_up() { echo "Morning already" } trap wake_up USR1 (sleep 20;kill -usr1 $$) & echo "Waiting for answer..." read ans echo "Here we are" The read however does not abort on modern systems. It used to be that the system call is cancaled on signal input. Ideally we would background the read, and kill it when the sleep completes, but we can't get the result of the read from a background session! ------------------------------------------------------------------------------- Read Line with its own history HISTFILE=~/.myscript.history # history file to use history -c # clear current history history -r # read from history file into memory myread() { read -e -p '> ' $1 history -s ${!1} # store input into history memory } trap 'history -a' EXIT trap 'exit 2' HUP INT QUIT ABRT TERM # append 'new' history to file myread line ------------------------------------------------------------------------------- Read Line with a default # USAGE: readline var prompt default bashversion=${BASH_VERSION%%.*} if [ ${bashversion:-0} -ge 4 ]; then ## bash4.0 has an -i option for editing a supplied value readline() { read -ep "${2:-"$prompt"}" -i "$3" "$1" } elif [ ${BASHVERSION:-0} -ge 2 ]; then # bash 2 and 3 readline() { history -s "$3" printf "Press up arrow to edit default value: '%s'\n" "${3:-none}" read -ep "${2:-"$prompt"}" "$1" } else # Other older bourne shells readline() { printf "Press enter for default of '%s'\n" "$3" printf "%s " "${2:-"$prompt"}" read eval "$1=\${REPLY:-"$3"}" } fi ------------------------------------------------------------------------------- Turn off input echo (password input) Bourne Shell... =======8<-------- read_noecho() { # A 'no-echo' TTY Password Reader (bourne sh) # Prepare Terminal if tty >/dev/null; then echo -n "$1 " >/dev/tty # output prompt stty_save=`stty -g` # save the terminal state trap 'stty "$stty_save"' EXIT # restore it on exit trap 'echo "===INTERUPT==="; exit 10' HUP INT QUIT ABRT TERM stty -echo # turn off echo fi # Do the read unset passwd # ensure it is not an environment variable! read -r passwd echo '' # echo return after reading # Return TTY to normal, cancel exit trap if tty >/dev/null; then stty "$stty_save" trap - EXIT HUP INT QUIT TERM fi } read_noecho "Password:" =======8<-------- BASH version... =======8<-------- read_noecho() { # A 'no-echo' TTY Password Reader (BASH) unset passwd # ensure it is not an environment variable! read -r -s -p "$1" passwd tty >/dev/null && echo '' # echo return afetr reading } =======8<-------- For more advanced password input see... https://antofthy.gitlab.io/info/crypto/passwd_input.txt ------------------------------------------------------------------------------- User Input Helpers Password password input see... https://antofthy.gitlab.io/info/crypto/passwd_input.txt ------------------------------------------------------------------------------- Read 'n' BINARY characters from file or tty WARNING: A NUL character can NOT be stored by the shell in a variable. As such raw binary strings can not be stored as strings by the shell. (see below). On GNU-Linux systems you can use "head -c" echo '1234567890' | head -c 5 12345 It can even handle NUL characters printf 'a\0bced' | head -c 3 | od -c 0000000 a \0 b 0000003 You cannot store NUL characters in a shell variable! BUT you can store character codes, including that of NUL. while true; do code=$(head -c1 - | od -An -tx1 | tr -d ' \011') echo "$code" [[ $code = 0a ]] && break # break on return done Sanitise posible binary input, for example when reading 'file magic'... magic=$(head -c 6 - | tr -d -c 0-9a-zA-Z) if [[ $magic != MyFile ]]; then ----- "dd" is more universally available (such as on Solaris), but it requires a lot of options (much like bash "read" does) n=10 input=`dd if=/dev/tty bs=1 count=$n 2>/dev/null` (The above is not setup to handle NUL) Good to read single characters from stdin in "raw" mode.. See below... This can also be used to extract say 3 bytes (count=3) at position 5 (skip=4) echo '1234567890' | dd bs=1 skip=4 count=3 2>/dev/null; echo '' 567 The 2>/dev/null junks the dd report it outputs to STDERR the final 'echo' is to add a extra return character to the end of the 3 bytes 'dd' extracts. ------------------------------------------------------------------------------- Read any character from a file stream (BASH-read) Example Solution... NOTES:... * IFS prevents the shell ignoring leading whitespace or separators. * -r is needed to disable backslash escaping. * -d or return will be regarded as EOL. * -s turns off echoing (without needing "stty") * -n1 or -N1 return just one character The loop exits (read returns false) only on EOF, timeout or interrupt. The "printf" built-in was used so we can output a real NUL character. printf 'a\tb\rc\nd\0e' | od -c 0000000 a \t b \r c \n d \0 e 0000011 =======8<-------- printf 'a\tb\rc\nd\0e' | while IFS= read -r -s -d '' -n1 char do if [[ "$char" == '' ]]; then echo -n "-NUL-" elif [[ "$char" == $'\r' ]]; then echo -n "-RETURN-" elif [[ "$char" == $'\n' ]]; then echo -n "-NEWLINE-" elif [[ "$char" == $'\t' ]]; then echo -n "-TAB-" else echo -n "$char" fi done; echo =======8<-------- result a-TAB-b-RETURN-c-NEWLINE-d-NUL-e perfect! If you want to use chacter codes (safer and more versitile) =======8<-------- printf 'a\tb\rc\nd\0e' | while IFS= read -r -s -d '' -N1 char do printf -v code '%02x' "'$char" # <-- not the extra ' in double quotes! case "$code" in 00) echo -n "-NUL-" ;; 0a) echo -n "-NEWLINE-" ;; 0d) echo -n "-RETURN-" ;; 09) echo -n "-TAB-" ;; [01]?) echo -n "-c$code-" ;; # any other control code *) echo -n "$char" ;; esac done; echo =======8<-------- The read will only return true if it actually reads a character! If read returns true BUT the variable "$char" is empty then either a NUL character was received, or a delimiter was received (which is turned off) If the -d was not given then a NEWLINE returns as a NUL delimiter, and the result will become... a-TAB-b-RETURN-c-NUL-d-NUL-e NOTE: You may want to turn off echo (using stty) instead of read -s Basically a fast typing user may type characters before the next read picks them up and outputs the character with no-echo (type-ahead echo). ------------------------------------------------------------------------------- Read any character with timeout (using BASH-read) It is very hard to differentiate between a timeout, NUL character, or EOF You need to look at the exit code to determine this. Note this does handle signal processing (^C for interupt or ^D for EOF). For that see the non-BASH method (using "stty" and "dd") below. Also see "info/crypto/passwd_input.txt" IFS= read -r -s -d '' -n1 -t 2 "${1:-KEY}" If a -t (timeout) option was also used, then you need to test if the return status from read more throughly. --- Using a timeout has four conditions to the read... Normal character ("$char" is not the empty string) printf 'a' | IFS= read -r -s -d '' -n 1 -t 2 char; echo status=$? status=0 echo "$char" a NUL character recieved ("$char" is assigned the empty string) printf '\0' | IFS= read -r -s -d '' -n 1 -t 2 char; echo status=$? status=0 [[ -n $char ]] && echo NUL NUL EOF recieved ("$char" not modified - error exit value) : | IFS= read -r -s -d '' -n 1 -t 2 char; echo status=$? status=1 Timeout (run this but don't type anything) IFS= read -r -s -d '' -n 1 -t 2 char; echo status=$? status=142 NOTE 142 = 128 + 14 or killed by SIGALRM You probably get something similar for broken pipe! ---- Key Polling Using a -t 0 is a polling read read -r -t 0 var This does not actually read anything. Its exit status is 0 (true) if there is something to read otherwise it returns 1 or false if there isn't or it is EOF. You can't distinguish between no input and EOF, until you actually read something. Another method I have found is to use a small perl script to make a IO library 'select()' system call to see if input is available before doing the "read". But that is not pure-BASH. ---- Non-Blocking Reads Read using a micro-timeout... read -r -t 0.00001 line This is a non-blocking read that times out very very quickly. If a line was read you get a exit status of 0 (okay) However it will only return a result if a full line was available If a full line was not available you get a timeout return (142) This happens even for a incomplete line followed by EOF, as no complete line is available. Add "-d ''" to get read everything, perhaps with "-n1" for one char at a time. ---- Select Using external program (perl) to do a select() system call See also https://antofthy.gitlab.io/software/#shell_select For more on using "read" see the section... "Bash "read" Notes" in... https://antofthy.gitlab.io/info/co-processing/general_hints.txt ------------------------------------------------------------------------------- Slurp up all input Just read all queued up input, if any, regardless of newlines, one character at a time... while IFS= read -r -s -d '' -n1 -t .01 key do [ $? -ge 128 ] && break # do something with $key (empty = NULL character) done ------------------------------------------------------------------------------- Read a single character (non-BASH-read) Simple method using "head -c" readkey() { old_stty=$(stty -g) stty raw -echo head -c 1 # get first character and echo it stty "$old_stty" } echo -n "Enter a character: " key=`readkey` echo "Thank you for typing \"$key\"" Wait for a 'y' or 'n' key yn=$( while ! head -c 1 | grep -i '[ny]'; do :; done ) -------- Reading NUL characters too... =======8<-------- #!/bin/sh readkey() { if tty >/dev/null; then save_stty=`stty -g` #stty -icanon -echo && /bin/stty eol ^A stty cbreak -echo fi key=`dd if=- bs=1 count=1 2>/dev/null` # read a key press tty >/dev/null && stty "$save_stty" echo "$key" } echo -n "Enter a character: " key=`readkey` echo "Thank you for typing \"$key\"" =======8<-------- For say a 4 second timed key input add this to the stty... min 0 time 40 OR add a -t 4 for the BASH The stty setting defaults for this is: min 1 time 0 NOTE: that stty modes are tricky, particulaly when giving it control characters as arguments. For example linux stty command will accept and argument of two characters "^A" to represent the single character ctrl-A, while other UNIX machines must actually recieve a single "ctrl-A" (character code 1) character, to work as expected. An alternative "readkey()" function presented from a eZine "Shell Corner" This also handles users inputing signal characters (like ^C) and even seems to handle a NUL (^@) character And can use stty -icanon -isig -echo min 0 time 40 for a timeout on the key read =======8<-------- readkey() { oldstty=`stty -g` #stty -icanon -isig -echo min 1 time 0 stty -icanon -isig -echo min 0 time 40 dd bs=1 count=1 <&0 2>/dev/null stty $oldstty } #key=`readkey` # read in a key from user and convert to character code echo "Enter a key" char=`readkey` #code=`echo "$char" | od -b | awk '{ print $2 }'` # Octal EG: 012 (linefeed) code=`echo "$char" | od -An -tx1 | tr -d ' '` # Hexadecimal EG: 0a #code=${char:+$(printf '%02x' "'$char'")} # bash built-in # Determine the action to take... case $char_code in '') echo "no key pressed" ;; 03) echo "Interupt!" ;; *) echo "'$char' (0x$code)" ;; esac =======8<-------- This uses the output of the stty itself to reset it to normal, turns off signals (you will have to look for interupt characters yourself). To avoid problem about just what character was pressed by the user, the character was immediately piped into a "od" command to convert to a octal character code. You can put the script into that special stty mode at the start, and exit that mode using a trap trap 'stty $oldstty' EXIT trap 'exit 10' HUP INT QUIT ABRT TERM =============================================================================== Terminal control during input.... ------------------------------------------------------------------------------- Using the ANSI alternate screen during input... That is switching to a different screen so that the users normal commands and results are not effected when you make by gross terminal updates. For example when editing files! echo -n $'\E[?47h'; ... do stuff on display ... echo -n $'\E[?47l'; This alt screen is styply used by vims. oYo ucan see this in terminfo using infocmp -C xterm | grep :te= There are other tricks with the terminal too, such as creating a non-scrolling area. ------------------------------------------------------------------------------- ANSI escaped capitals... In some situations XTerms can enable 'ModifyOtherKeys' mode where capital letters output ANSI escape sequences rather that the capital letter. For example typing 'A' results in '\e[27;2;65~' (10 character input) Where the '2' means shifted and '65' is the character code for capital-A. VIM for example is using this 'ModifyOtherKeys' mode to allow it to differentiate between various shift/control/meta keys, and in some specific cases this mode 'leaks' out to external programs (like password readers). The Vim Bug was fixed when I reported it. See "Password Input for File Decryption Failing" in https://antofthy.gitlab.io/info/vi/vim_hints.txt See document "XTerm Control Sequences", "Set/reset key modifier options", and later "Unset", on page 19, and the results of this in "Alt and Meta Keys" on page 31.... https://invisible-island.net/xterm/ctlseqs/ctlseqs.pdf A solution is to ensure you disable this mode before attempting to read your input. if tty >/dev/null; then case "$TERM" in xterm*) echo -n $'\e[>4n' >/dev/tty ;; esac fi -------------------------------------------------------------------------------