Press any key to abort in 5 seconds - bash

Hi I'm trying to implement an event that will happen after a 5 second countdown, unless a key is pressed. I have been using this code, but it fails if I press enter or space. It fails in the sense that enter or space is detected as "".
echo "Phoning home..."
key=""
read -r -s -n 1 -t 5 -p "Press any key to abort in the next 5 seconds." key
echo
if [ "$key" = "" ] # No Keypress detected, phone home.
then python /home/myuser/bin/phonehome.py
else echo "Aborting."
fi
After reading this post,
Bash: Check if enter was pressed
I gave up and posted here. I feel like there must be a better way than what I have tried to implement.

The read manual says:
The return code for read is zero, unless end-of-file is encountered
or read times out.
In your case, when the user hits any key within allowed time you wish to abort else continue.
#!/bin/bash
if read -r -s -n 1 -t 5 -p "TEST:" key #key in a sense has no use at all
then
echo "aborted"
else
echo "continued"
fi
Reference:
Read Manual
Note:
The emphasis in the citation is mine.

The accepted answer in the linked question covers the "detecting enter" component of the question. You look at the exit code from read.
As to handling spaces there are two answers.
The problem with space is that under normal circumstances read trims leading and trailing whitespace from the input (and word-splits the input) when assigning the input to the given variables.
There are two ways to avoid that.
You can avoid using a custom named variable and use $REPLY instead. When assigning to $REPLY no whitespace trimming or word-splitting is performed. (Though looking for this just now I can't actually find this in the POSIX spec so this may be a non-standard and/or non-portable expansion of some sort.)
Explicitly set IFS to an empty string for the read command so it doesn't perform and whitespace trimming or word-splitting.
$ IFS= read -r -s -n 1 -t 5 -p "Press any key to abort in the next 5 seconds." key; echo $?
# Press <space>
0
$ declare -p key
declare -- k=" "
$ unset -v k
$ IFS= read -r -s -n 1 -t 5 -p "Press any key to abort in the next 5 seconds." key; echo $?
# Wait
1
$ declare -p key
-bash: declare: k: not found

Related

Bash read command - how to accept control characters

I am using the following to read a bunch of parameters from the user.
read -p "Name`echo $'\n> '`" NAME
All my parameters have default values. I want to provide an option to the user to skip providing values at any point. i.e. user might provide values for first 3 parameters and press Ctrl+s to skip entering values for the rest of them.
How can I trap Ctrl+s?
Ctrl+S is the terminal scroll lock character, and is not immediately available. You have two options:
Work with the system, use the system standard key combos, and make life easier for yourself and everyone else.
Fight the system, insist on using this key combo, do anything it takes to make it happen, and then live with the consequences.
If you want to work with the system, consider using a blank line and/or Ctrl+D, which is already extensively used to end input. This is easy and robust:
if read -p "Name (or blank for done)"$'\n> ' name && [[ $name ]]
then
echo "User entered $name"
else
echo "User is done entering things"
fi
Alternatively, here's a start for fighting the system:
#!/bin/bash
settings=$(stty -g)
echo "Enter name or ctrl+s to stop:"
stty stop undef # or 'stty raw' to read other control chars
str=""
while IFS= read -r -n 1 c && [[ $c ]]
do
[[ $c = $'\x13' ]] && echo "Ctrl+S pressed" && break
str+="$c"
done
echo "Input was $str"
stty "$settings"
This will correctly end when the user hits Ctrl+S on a standard configuration, but since it doesn't work with the system, it needs additional work to support proper line editing and less common configurations.
Not sure what you mean by trapping. The user can input Ctrl+S by typing Ctrl+V and then Ctrl+S, and your script can then do the check:
if [[ $NAME == '^S' ]]; then
...
# skip asking for more values and continue with default values
fi

Bash Unix: How can i pause a process without using sleep command

Hi im trying to pause the execution of a process so that a user cannot make multiple entries until 5 seconds have elapsed. I tried using sleep but sleep simply stops and then executes all the inputs the user ran while the process was asleep, i dont want there to be any input read from when the program was sleeping.
example of what i want: a chat bot
input 1: hi
output: "reply from program"
input 2 (before 5 seconds is up): "whats new"
-no output-
input 3 (5 seconds have passed): "how are you"
output: im fine.
example of what sleep command does:
input 1. "hi"
output "hey"
input 2 (before 5 seconds): "whats new"
-no output- waits
input 3: "how are you"
output: "not much is new"
output: "im fine"
You can read and throw away the user's input after the 5 seconds is up
# 1.
read -p "prompt 1: " first_thing
sleep 4
# this reads and ignores whatever the user has typed during the sleep
while read -t 1 _garbage; do :; done
# 2.
read -p "prompt 2: " next_thing
reads -t 1 option is a timeout of one second if there's nothing to read.
Testing
with input
$ read -p "prompt 1: " first_thing; sleep 4; while read -t 1 _garbage; do :;done ; read -p "prompt 2: " next_thing
prompt 1: foo
bar
baz
prompt 2: qux
$ echo $first_thing $next_thing
foo qux
no "extra" input before the 2nd read -- process does not "hang" awaiting input
$ read -p "prompt 1: " first_thing; sleep 4; while read -t 1 _garbage; do :;done ; read -p "prompt 2: " next_thing
prompt 1: hello
prompt 2: world
$ echo $first_thing $next_thing
hello world
This is by no means easy, and you need to be more specific about how you expect the script to respond to keyboard input.
There is no way to "lock" the keyboard; the user can continue punching keys as they see fit. If you are content for the keys to be echoed, but you want the input to be ignored, you could do something like the following:
# Beware! Read the entire answer; don't just use this command
timeout 5 bash -c 'while :;do read -s -d ""; done'
The timeout utility runs a command, killing it when the specified number of seconds have elapsed; the specified number may be a decimal fraction. The loop around the read command is necessary because the read would other terminate as soon as the Enter key is pressed; while :; do is a standard idiom for "loop forever".
The timeout command is part of Gnu coreutils. If you don't have it, perhaps because you are using a BSD derivative, you can probably find alternatives. There is a FreeBSD command, probably available on other BSDs including Mac OS X, called timelimit; I believe the correct invocation would be to replace timeout 5 with timelimit -t 5 -s9, but I don't have any easy way of testing.
You need to get the read command to actually read input immediately, as opposed to waiting until the Enter key is pressed. Otherwise, the typed input will still be available to the next command after the read is terminated.
There are several ways to do this. One is to use the -n 1 flag to cause the read to return after each character; another one is to use -d "" to set the end of input character to NUL, which has the side effect of putting read into character-at-a-time mode.
Also, you will probably want to suppress echo of the keys pressed while you are in the read loop. You can do that by adding the -s flag to the read command, but again that will have the side effect of leaving the terminal in "no echo" mode when the read command is interrupted. [Note 1]
Unfortunately, you'll probably find that the terminal settings have been permanently changed, because when read is killed by the timeout command, it doesn't have a chance to restore the terminal settings. So you'll end up with a terminal which doesn't echo, doesn't handle backspace and other line-editing commands, and doesn't honor Ctrl-D, amongst other issues.
To avoid this problem, you need to save and restore the terminal settings. You can do that with the stty command, as follows:
# Save the terminal settings
saved=$(stty -g)
# Ignore input for 5 seconds, suppressing echo
timeout 5 bash -c 'while :;do read -s -d ""; done'
# Restore the terminal settings
stty "$saved"
If you don't suppress echo, you'll find that your input prompt may appear on the same line as the ignored input. You could avoid that by outputting a "carriage return / erase to end of line" control sequence before the prompt:
tput cr; tput el; read -p "Give me some input: "
The answer of #glenn is a good answer!
Maybe you don't know in front how long you are going to sleep (you are calling
some other functions).
When you just want to flush the input queue, you can try a similar approach:
echo "What is your name?"
read name
echo "Please be quiet for 5 seconds"
sleep 5
smalltime=0.000001
read -t ${smalltime} -s garbage
while [ -n "${garbage}" ]; do
echo "Flushing $garbage"
read -t ${smalltime} -s garbage
done
echo "Hello ${name}, please say something else"
read y
echo "You said $y"

How to just input 'y' without having to press ENTER?

Using this portion of a bash script as an example
{
read -p "Do you want to update the tv feed? [y/n/q] " ynq
case $ynq in
[Yy]* ) rm ~/cron/beeb.txt; /usr/bin/get-iplayer --type tv>>~/cron/beeb.txt;;
[Nn]* ) echo;;
[Qq]* ) exit;;
* ) echo "Please answer yes or no. ";;
esac
}
How do I get it so that you can press y and not have to press Enter for it to be accepted please?
Add -n 1 to the read command's options. From the bash manpage:
-n nchars
read returns after reading nchars characters rather than
waiting for a complete line of input.
BTW, you should also double-quote "$ynq" -- sometimes users will just press return, which can cause weird behavior if the variable isn't double-quoted. Also, note that read -n is a bash extension, so make sure you're using bash (i.e. #!/bin/bash or similar for the first line of the script), not a brand-x shell (#!/bin/sh or similar).
Use -n1 with read to specify max number of input length to 1:
read -n1 -p "Do you want to update the tv feed? [y/n/q] " ynq
I am on Mac and using read -n1 $user_decision doesn't do the trick for some reason in bash, sh, or zsh. So, I am using this which works across all:
#!/bin/zsh
# -k1 = First char pressed without waiting, for /r or /n.
# -t3 = timeout for 3 seconds
# -s = prevent outputting the input back to stdout.
echo "Press any letter..."
read -t3 -k1 -s user_decision
# Prints 1st arbitrary keypress entered during 3s timeout
echo $user_decision # EG: "y", or "n" for instance.
To simplify, you can just use read -k1 user_decision to get precisely what you requested set into the value for the variable name $user_decision, without waiting for /r or /n (hitting enter or return).

Bash script countdown timer needs to detect any key to continue

I need to listen for any key press in a countdown timer loop. If any key is pressed then the countdown timer should break out of it's loop. This mostly works except for the enter key just makes the countdown timer go faster.
#!/bin/bash
for (( i=30; i>0; i--)); do
printf "\rStarting script in $i seconds. Hit any key to continue."
read -s -n 1 -t 1 key
if [[ $key ]]
then
break
fi
done
echo "Resume script"
I just can't seem to find any examples of how to detect that enter key anywhere online.
I think based on the return code of read, there is a work around for this problem. From the man page of read,
The return code is zero, unless end-of-file is encountered, read times out,
or an invalid file descriptor is supplied as the argument to -u.
The return code for timeout seems to be 142 [verified in Fedora 16]
So, the script can be modified as,
#!/bin/bash
for (( i=30; i>0; i--)); do
printf "\rStarting script in $i seconds. Hit any key to continue."
read -s -n 1 -t 1 key
if [ $? -eq 0 ]
then
break
fi
done
echo "Resume script"
The problem is that read would by default consider a newline as a delimiter.
Set the IFS to null to avoid reading upto the delimiter.
Say:
IFS= read -s -N 1 -t 1 key
instead and you'd get the expected behavior upon hitting the Enter key during the read.

sentence as user input - multiple times from terminal - bash script

I am trying to send lines from terminal to a text file multiple times using the following script. After writing the first line and its description in 2nd line, the script asks user whether he wants to enter another line or not. If yes, then user writes the 3rd line, 4th line and so on...
my problem is that after 2nd line, i.e. starting from 3rd line, the script writes only the first word, not the full sentence. How do I solve this ?
function ml() {
echo $# >> $HOME/path/to/file/filename
echo -n "Enter description and press [ENTER]: "
read description
echo -e '\n[\t]' $description >> $HOME/path/to/file/myfile
while true
do
read -p "Add another line?y?n" -n 1 -r
echo -e "\n"
if [[ $REPLY =~ ^[Yy]$ ]]
then
echo -n "Enter another line and press [ENTER]: "
read -a meaning
echo -e "[\t]" $meaning >> $HOME/path/to/file/myfile
else
break
fi
done
echo % >> $HOME/path/to/file/myfile
}
also I would like to have another modification in the code
read -p "Add another line?y?n" -n 1 -r
instead of asking y/n input, can it be done that after inserting the first two line, every ENTER will ask for another line input and pressing ESCAPE will terminate the script?
This is because in your second call to read, you are using the -a argument which does:
The words are assigned to sequential indices of the array variable aname, starting at 0. aname is unset before any new values are assigned. Other name arguments are ignored.
That appears to be not what you want.

Resources