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.
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
}
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
Related
I am trying to write a program which reads from stdin and also receives user input from the tty.
I would like to disable rendering of user input because it messes with my menu system and causes flickering if I redraw to remove it. However I cannot seem to use stty -echo if the script recieves input from stdin.
Here is a simplified example of the script:
trapinput
#!/bin/bash
hideinput()
{
if [ -t 0 ]; then
echo "Is tty"
save_state=$(stty -g)
stty -echo -icanon time 0 min 0
echo -ne "\e[?1049h\r" 1>&2;
else
echo "is not tty"
fi
}
cleanup()
{
if [ -t 0 ]; then
stty "$save_state"
echo -ne "\e[?1049l" 1>&2;
echo "exit tty"
else
echo "is not tty"
fi
}
trap cleanup EXIT
trap hideinput CONT
hideinput
input="$(< /dev/stdin)";
echo "$input"
while true;
do
read -r -sn1 < /dev/tty;
read -r -sn3 -t 0.001 k1 < /dev/tty;
REPLY+=$k1;
echo $REPLY
done
hello.txt
helloworld!
running $ ./trapinput will echo "Is tty" on start and "exit tty" when killed along with running the rest of the program as I would expect. It also prevents the user input from being displayed directly allowing me to print it on the screen in the correct location.
However if I run $ echo "test" | ./trapinput or $ ./trapinput < hello.txt it will echo "is not tty" and stty -echo is not set causing user input to be displayed where I do not want it.
How can I disable rendering of user input but retain the ability to pipe in text/use file redirection?
How can I disable rendering of user input but retain the ability to pipe in text/use file redirection?
Disable the echo on where you from take the input. Do:
trap 'cleanup < /dev/tty' EXIT
trap 'hideinput < /dev/tty' CONT
hideinput </dev/tty
You could also open a file descriptor specific for input exec 10</dev/tty, etc.
I want to build a stopwatch in Bash, with a pause feature. It should display an incrementing counter, like this one does, but pause it when I hit the "p" key.
How should I implement that? If I wait for user input with read I can't refresh the counter on the screen at the same time. Putting the read inside a loop, with a timeout, is my best plan so far, but it's non-trivial to use a timeout less than one second, which is what I would need here. (It's not supported by read or GNU timeout.) Interrupts would work, but I'd like to support arbitrary keys like "p" and "x".
Is there a reasonably simple way to achieve this?
Print to console while waiting for user input
Write one function that creates the output (example with: counter or if you like spin).
Write one function to read in user commands (readCommand)
Call both functions in a loop
Set timeouts so, that key presses are read soon enough. (sleep .1 and read -t.1)
function readCommand(){
lastCommand=$1
read -t.1 -n1 c;
if [ "$c" = "p" ]
then
printf "\n\r";
return 0
fi
if [ "$c" = "g" ]
then
printf "\n\r";
return 1
fi
return $lastCommand
}
function spin(){
for i in / - \\ \| ;
do
printf "\r$i";
sleep .1;
done
}
function countUp(){
currentCount=$1
return `expr $currentCount + 1`
}
function counter(){
countUp $count
count=$?
printf "\r$count"
sleep .1;
}
command=1
count=0
while :
do
if [[ $command == 1 ]]
then
counter
fi
readCommand $command
command=$?
done
The counter will stop if user presses 'p' and go on if user presses 'g'
Simple script with file descriptor and simple input redirection, leaving no temporary files to cleanup. The waiting is done by using read parameter -t.
counter() {
while ! read -t 0.05 -n1 _; do
printf '\r\t%s' "$(date +%T.%N)"
done
}
{
IFS= read -p "Your name, Sir?"$'\n' -r name
echo >&3
} 3> >(counter "$tmp")
echo "Sir $name, we exit"
Example output:
Your name, Sir?
2:12:17.153951623l
Sir Kamil, we exit
I have made a change in the code you refer.
...
while [ true ]; do
if [ -z $(cat /tmp/pause) ]; then
STOPWATCH=$(TZ=UTC datef $DATE_INPUT $DATE_FORMAT | ( [[ "$NANOS_SUPPORTED" ]] && sed 's/.\{7\}$//' || cat ) )
printf "\r\e%s" $STOPWATCH
sleep 0.03
fi
done
So what you need to do now is a shell script that waits the "p" char from stdin and writes 1 > /tmp/pause or clean /tmp/pause to get he stopwatch paused or working.
something like:
while read char;
do
if [ $char == "p" ]; then
if [ -z $(cat /tmp/pause) ];then
echo 1 > /tmp/pause
else
echo > /tmp/pause
fi
char=0
fi
done < /dev/stdin
I have an input that times out after a few seconds, but to make it less jarring I would like it to display the time remaining. How can I do this?
I don't think it can be done with read -t, but this script accomplishes a countdown on another line as a background process:
#!/bin/bash
function displayCountdown {
((remaining=$1))
while [ $remaining -gt 0 ]
do
tput sc # save cursor pos
tput cuu1 # go up one line
tput cub 80 # go 80 chars left
tput el # clear to eol
( echo -n "$remaining second(s) remaining" ) >&2
tput rc # restore saved cursor pos
((remaining=remaining-1))
sleep 1
done
echo
}
NUM_SECONDS=5
## the first echo is needed
echo ; displayCountdown $NUM_SECONDS & read -t $NUM_SECONDS ; RC=$? ; kill -9 $! ; wait $! 2>/dev/null
echo "Got ($RC): $REPLY"
exit 0
It's not perfect. For instance, when pressing the delete key, it sometimes produces ^R's and messes up the terminal a bit. But, if you decide to use this, maybe there is some stty setting you can use to remedy this.
I tried this on a Mac. I haven't tried this on Linux.
I have this bash script which basically starts the web and selenium servers with progress indicator. Since it takes some time to selenium server to start I'm checking the status in an infinite loop.
The problem is, while waiting for it to start I press keys accidentaly it's displayed in the screen and if the loop ends (times out) it's also displayed in the command prompt too.
I want to disable all user input (except the control keys of course) while inside the loop:
start_selenium() {
echo -n "Starting selenium server"
java -jar ${selenium_jar} &> $selenium_log &
# wait for selenium server to spin up! (add -v for verbose output)
i=0
while ! nc -z localhost 4444; do
sleep 1
echo -n "."
((i++))
if [ $i -gt 20 ]; then
echo
echo -e $bg_red$bold"Selenium server connection timed out"$reset
exit 1
fi
done
}
The stty invocations are from http://www.unix.com/shell-programming-and-scripting/84624-nonblocking-i-o-bash-scripts.html
This still respects Ctrl-C, but doesn't show input, and consumes it so it's not left for the shell.
#!/bin/bash
hideinput()
{
if [ -t 0 ]; then
stty -echo -icanon time 0 min 0
fi
}
cleanup()
{
if [ -t 0 ]; then
stty sane
fi
}
trap cleanup EXIT
trap hideinput CONT
hideinput
n=0
while test $n -lt 10
do
read line
sleep 1
echo -n "."
n=$[n+1]
done
echo
Use stty to turn off keyboard input.
stty -echo
#### Ur Code here ####
stty echo
-echo turns off keyboard input and stty echo reenables keyboard input.
I need to write an infinite loop that stops when any key is pressed.
Unfortunately this one loops only when a key is pressed.
Ideas please?
#!/bin/bash
count=0
while : ; do
# dummy action
echo -n "$a "
let "a+=1"
# detect any key press
read -n 1 keypress
echo $keypress
done
echo "Thanks for using this script."
exit 0
You need to put the standard input in non-blocking mode. Here is an example that works:
#!/bin/bash
if [ -t 0 ]; then
SAVED_STTY="`stty --save`"
stty -echo -icanon -icrnl time 0 min 0
fi
count=0
keypress=''
while [ "x$keypress" = "x" ]; do
let count+=1
echo -ne $count'\r'
keypress="`cat -v`"
done
if [ -t 0 ]; then stty "$SAVED_STTY"; fi
echo "You pressed '$keypress' after $count loop iterations"
echo "Thanks for using this script."
exit 0
Edit 2014/12/09: Add the -icrnl flag to stty to properly catch the Return key, use cat -v instead of read in order to catch Space.
It is possible that cat reads more than one character if it is fed data fast enough; if not the desired behaviour, replace cat -v with dd bs=1 count=1 status=none | cat -v.
Edit 2019/09/05: Use stty --save to restore the TTY settings.
read has a number of characters parameter -n and a timeout parameter -t which could be used.
From bash manual:
-n nchars
read returns after reading nchars characters rather than waiting for a complete line of input, but honors a delimiter if fewer than nchars characters are read before the delimiter.
-t timeout
Cause read to time out and return failure if a complete line of input (or a specified number of characters) is not read within timeout seconds. timeout may be a decimal number with a fractional portion following the decimal point. This option is only effective if read is reading input from a terminal, pipe, or other special file; it has no effect when reading from regular files. If read times out, read saves any partial input read into the specified variable name. If timeout is 0, read returns immediately, without trying to read any data. The exit status is 0 if input is available on the specified file descriptor, non-zero otherwise. The exit status is greater than 128 if the timeout is exceeded.
However, the read builtin uses the terminal which has its own settings. So as other answers have pointed out we need to set the flags for the terminal using stty.
#!/bin/bash
old_tty=$(stty --save)
# Minimum required changes to terminal. Add -echo to avoid output to screen.
stty -icanon min 0;
while true ; do
if read -t 0; then # Input ready
read -n 1 char
echo -e "\nRead: ${char}\n"
break
else # No input
echo -n '.'
sleep 1
fi
done
stty $old_tty
Usually I don't mind breaking a bash infinite loop with a simple CTRL-C. This is the traditional way for terminating a tail -f for instance.
Pure bash: unattended user input over loop
I've done this without having to play with stty:
loop=true
while $loop; do
trapKey=
if IFS= read -d '' -rsn 1 -t .002 str; then
while IFS= read -d '' -rsn 1 -t .002 chr; do
str+="$chr"
done
case $str in
$'\E[A') trapKey=UP ;;
$'\E[B') trapKey=DOWN ;;
$'\E[C') trapKey=RIGHT ;;
$'\E[D') trapKey=LEFT ;;
q | $'\E') loop=false ;;
esac
fi
if [ "$trapKey" ] ;then
printf "\nDoing something with '%s'.\n" $trapKey
fi
echo -n .
done
This will
loop with a very small footprint (max 2 millisecond)
react to keys cursor left, cursor right, cursor up and cursor down
exit loop with key Escape or q.
Here is another solution. It works for any key pressed, including space, enter, arrows, etc.
The original solution tested in bash:
IFS=''
if [ -t 0 ]; then stty -echo -icanon raw time 0 min 0; fi
while [ -z "$key" ]; do
read key
done
if [ -t 0 ]; then stty sane; fi
An improved solution tested in bash and dash:
if [ -t 0 ]; then
old_tty=$(stty --save)
stty raw -echo min 0
fi
while
IFS= read -r REPLY
[ -z "$REPLY" ]
do :; done
if [ -t 0 ]; then stty "$old_tty"; fi
In bash you could even leave out REPLY variable for the read command, because it is the default variable there.
I found this forum post and rewrote era's post into this pretty general use format:
# stuff before main function
printf "INIT\n\n"; sleep 2
INIT(){
starting="MAIN loop starting"; ending="MAIN loop success"
runMAIN=1; i=1; echo "0"
}; INIT
# exit script when MAIN is done, if ever (in this case counting out 4 seconds)
exitScript(){
trap - SIGINT SIGTERM SIGTERM # clear the trap
kill -- -$$ # Send SIGTERM to child/sub processes
kill $( jobs -p ) # kill any remaining processes
}; trap exitScript SIGINT SIGTERM # set trap
MAIN(){
echo "$starting"
sleep 1
echo "$i"; let "i++"
if (($i > 4)); then printf "\nexiting\n"; exitScript; fi
echo "$ending"; echo
}
# main loop running in subshell due to the '&'' after 'done'
{ while ((runMAIN)); do
if ! MAIN; then runMain=0; fi
done; } &
# --------------------------------------------------
tput smso
# echo "Press any key to return \c"
tput rmso
oldstty=`stty -g`
stty -icanon -echo min 1 time 0
dd bs=1 count=1 >/dev/null 2>&1
stty "$oldstty"
# --------------------------------------------------
# everything after this point will occur after user inputs any key
printf "\nYou pressed a key!\n\nGoodbye!\n"
Run this script