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

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"

Related

In bash print to line above terminal output

EDIT: Corrected process/thread terminology
My shell script has a foreground process that reads user input and a background process that prints messages. I would like to print these messages on the line above the input prompt rather than interrupting the input. Here's a canned example:
sleep 5 && echo -e "\nINFO: Helpful Status Update!" &
echo -n "> "
read input
When I execute it and type "input" a bunch of times, I get something like this:
> input input input inp
INFO: Helpful Status Update!
ut input
But I would like to see something like this:
INFO: Helpful Status Update!
> input input input input input
The solution need not be portable (I'm using bash on linux), though I would like to avoid ncurses if possible.
EDIT: According to #Nick, previous lines are inaccessible for historical reasons. However, my situation only requires modifying the current line. Here's a proof of concept:
# Make named pipe
mkfifo pipe
# Spawn background process
while true; do
sleep 2
echo -en "\033[1K\rINFO: Helpful Status Update!\n> `cat pipe`"
done &
# Start foreground user input
echo -n "> "
pid=-1
collected=""
IFS=""
while true; do
read -n 1 c
collected="$collected$c"
# Named pipes block writes, so must do background process
echo -n "$collected" >> pipe &
# Kill last loop's (potentially) still-blocking pipe write
if kill -0 $pid &> /dev/null; then
kill $pid &> /dev/null
fi
pid=$!
done
This produces mostly the correct behavior, but lacks CLI niceties like backspace and arrow navigation. These could be hacked in, but I'm still having trouble believing that a standard approach hasn't already been developed.
The original ANSI codes still work in bash terminal on Linux (and MacOS), so you can use \033[F where \033 is the ESCape character. You can generate this in bash terminal by control-V followed by the ESCape character. You should see ^[ appear. Then type [F. If you test the following script:
echo "original line 1"
echo "^[[Fupdated line 1"
echo "line 2"
echo "line 3"
You should see output:
updated line 1
line 2
line 3
EDIT:
I forgot to add that using this in your script will cause the cursor to return to the beginning of the line, so further input will overwrite what you have typed already. You could use control-R on the keyboard to cause bash to re-type the current line and return the cursor to the end of the line.

Korn Shell - How can I make “Press any key to continue”

I am writing korn shell script. I need to put funtionality called "Press any key to continue" to execute rest of the script.How can I achieve this?
Thanks in advance
You can use read command:
read -n1 -r -p "Press space to continue..." key
if [ "$key" = '' ]; then
# Space pressed, do something
# echo [$key] is empty when SPACE is pressed # uncomment to trace
else
# Anything else pressed, do whatever else.
# echo [$key] not empty
fi
This is very simple script.
#!/usr/bin/ksh
echo "First Method"
read -s -n 1 -p "Press any key to continue..."
# insert echo here for cleaner output
echo
echo "Second Method"
echo "Press any key to continue..."
read -s -n 1 any_key
echo "Now exiting"
exit 0
Information about the read command:
To get input from the keyboard, you use the read command. The read command takes input from the keyboard and assigns it to a variable. Here is an example:
echo -n "Enter some text > "
read text
echo "You entered: $text"
read is the command for input (usually form terminal). There is (nearly) no way around the <Enter>, its the way to tell read to start processing the input.
If you want to start processing after whatever keystroke, you have to dive deep into terminal settings via stty, changing to raw mode and .....
And it is likely your terminal is in foul mood when you are coming out of that.
The effort to avoid side effects is usually not worth the effect.
Think about, you changed the terminal mode, waiting for a keystroke, do you allow Ctrl-C, backspace, ESC, ... to be keystroke or do they need special handling? You changed the terminal mode, you are respnsible now. How to set the terminal to a sane state if the process has been killed , ....
If you are up for a big challenge, go for it :-)
OK, I understand why my post was deleted. Here I go again.
What I did is to create a function 'readOne' and call it when needed.
readOne () {
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"
echo
}
Here is the explanation from the author :
The tty driver controls how input lines that you type are delivered to programs. Normally a tty driver will wait until a complete line is available. It also handles stuff like backspace so that program doesn'y need to. The stty command lets you change the way the tty driver works. "stty -a" will display all of the settings. You should do that to see what's available.
"stty -g" displays all of the settings too. But it's encoded and you can't understand the output. But you can save the output and feed it back into the stty command. So:
oldtty='stty -g'
stty $oldstty
will save and restore the original settings of the tty driver.
stty -icanon -echo min 1 time 0
is setting some options in the tty driver. -icanon turns off all special character processing. Now a backspace will be passed to the program rather than being processed. And a carriage return won't terminate a line. So now min and time control when a read has finished. "min 1" says we need at least one character. "time 0" means that we won't wait a while before completing a read. So each read from the program may return after just one character.
the -echo just turns off echo. I would not have done that.
dd is a program that is prepared to read data that is not organized into lines. This dd will read one block (count=1) of data. And that block will be one character in length (size=1). So the dd will read one character and return to the script.
The final echo moves the cursor to the next line.

Press any key to abort in 5 seconds

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

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).

How can I pause in zsh?

I want to write this bash loop for zsh
while true; do echo "print something"; read -p "pause"; done
This loop echos, then waits for the user to press enter. If I enter it as is, the read statement doesn't pause, causing zsh to infinitely echo "print something" without waiting for the user to press enter.
In zsh:
read -s -k '?Press any key to continue.'
From man zshbuiltins:
-s Don't echo back characters if reading from the terminal.
-k Read only one character.
name?prompt Name is omitted, thus user input is stored in the REPLY variable (and we ignore it). The first argument contains a ?, thus the remainder of this word is used as a prompt on standard error when the shell is interactive.
To include a newline after the prompt:
read -s -k $'?Press any key to continue.\n'
$'' is explained under QUOTING in man zshmisc.
Finally, a pause function that takes an arbitrary prompt message in a script that does what the OP asks:
#!/usr/bin/env zsh
pause() read -s -k "?$*"$'\n'
while true; do
echo "print something"
pause "pause"
done
Since this is about the only search result I could find, and I found it helpful but still a bit confusing, here is another way of putting it: If all you want to do is echo a line of text and wait for the user to press enter ...
read \?"I am waiting for you to press [Enter] before I continue."
It looks like -p does something different in zsh. You will probably need something like read some_variable\?pause.
#!/bin/zsh
pause()
{
echo "$*"; read -k1 -s
}
now we can call the function with any prompt text:
pause "paused! press any key to continue"
pause "you can write anything here :)"
If you want a way that works in both bash and zsh, and ensures I/O to/from the terminal:
# Prompt for a keypress to continue. Customise prompt with $*
function pause {
>/dev/tty printf '%s' "${*:-Press any key to continue... }"
[[ $ZSH_VERSION ]] && read -krs # Use -u0 to read from STDIN
[[ $BASH_VERSION ]] && </dev/tty read -rsn1
printf '\n'
}
export_function pause

Resources