I wanted to do a shell function which countdown to n seconds (10 for exemple) and after that, continue the script.
I tried with the sleep function but it does stop the script entirely.
I want something like when the user input "y" during this countdown, it will stop the countdown and do something particular (much like an "interrupt").
And if the countdown finishes without any user input, the script continues.
Thank you !
**UPDATE* *
#Krzysztof Księżyk That's exactly what i wanted !
One difference, if i want the read return only if "Y" is the input how can i do that ? i already tried with the -d and -a...
Here is my code :
label="NTFS"
read -n 1 -t 5 MS
if [ -z "$MS" ]
then
echo "You don't input value, default will be taken"
else
echo -e "\nYou pressed 'Y' and want change default backup device."
read -p "Please input label of your secondary backup device: " secondary_label
label=$secondary_label
fi
echo "the choosen backup device label is $label"
You can use read command, eg.
read -n 1 -t 10
It will wait up to 10 second just for 1 character.
** UPDATE **
modified solution based on extra info from author
CNT=0
while [ $CNT -lt 10 ]; do
(( CNT++ ))
read -s -n 1 -t 1 -r KEY
if [ "$KEY" == 'y' ]; then
echo 'doing something'
else
echo 'doing nothing'
fi
done
If you want an interrupt, you probably want to trap SIGINT:
#!/bin/sh
trap : SIGINT
echo begin
for((i=0;i<10;i++)); do printf "$i\r"; sleep 1; done &
wait || kill $!
echo
echo end
The script counts to 10, but the timer aborts when the user sends a SIGINT (eg ctrl-C)
Related
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'm learning bash scripting and I want to call a function every 5 seconds for x number of seconds. The x is determined by the command line argument when the script was run. I've tried watch but it seems to go on forever, I don't seem to have an ability to return after x seconds. There might be a sleep way but it seems clumsy and I have to deal with local drift. Is there an elegant solution?
My code:
#!/bin/bash
if [ $# -ne 1 ]; then # only 1 command line arg allowed
echo "Incorrect arguments"
exit 1
elif ! [[ $1 =~ ^[0-9]+$ ]]; then # arg must be digit
echo "Argument must be a positive number"
exit 1
fi
ask()
{
OUTPUT=$(snmpwalk -v2c -c community server oib)
CLEANOUTPUT="${OUTPUT:32}%"
echo $CLEANOUTPUT
}
#export -f ask
#watch -n5 ask
The SECONDS variable counts the number of seconds since bash was started:
#!/bin/bash
while (( SECONDS <= 20 ))
do
echo "Running something"
sleep 5 || break
done
echo "Done"
This simplistic method will sleep 5 after the last run even when you know it'll go over 20 seconds, and it does not try to account for the run time of the command (if the command runs for 2 seconds, it'll end up starting up once every 7 seconds).
If you want to be a little bit more accurate with the timing, you can put a sleep into the background, do whatever work is required, then use wait, which will halt foreground processing until the sleep exits. While accuracy won't be perfect, at least the time taken by the work part is not included.
Something like:
#!/bin/bash
LOOPTIMES=3
REPEATEVERY=5 # seconds
for (( count=0 ; $count < $LOOPTIMES ; count++ ))
do
/bin/sleep $REPEATEVERY &
echo "Doing work $count..."
# do whatever
wait # this will wait until the sleep exits
done
exit 0
#!/bin/bash
if [ $# -ne 1 ]; then # only 1 command line arg allowed
echo "Incorrect arguments"
exit 1
elif ! [[ $1 =~ ^[0-9]+$ ]]; then # arg must be digit
echo "Argument must be a positive number"
exit 1
fi
iterations=$(($1/5))
for i in $(seq 1 $iterations);
do
OUTPUT=$(snmpwalk -v2c -c community server oib)
CLEANOUTPUT="${OUTPUT:32}%"
echo $CLEANOUTPUT
sleep 5
done
when I use read statement in shell
read -n 1 -s -t 5 -p "Starting the script in 5 seconds. Press any key to stop!" yn
How to check if any key is pressed or not , if a key is pressed then the script must exit otherwise , the script must continue ?
You can use loop and limit read's time by one second:
#!/bin/bash
shouldStop=0
for (( i=5; i>0; i--)); do
printf "\rStarting script in $i seconds. Press any key to stop!"
read -s -n 1 -t 1 key
if [ $? -eq 0 ]
then
shouldStop=1
fi
done
if [ $shouldStop==1 ]
then
printf "do not run script"
else
printf "run script"
fi
Simply:
read -n 1 -s -t 5 -p "Starting the script in 5 seconds. Press any key to stop!" && \
exit 1
When a key is read, read returns 0 which would allow && to process the next statement exit 1. This would end your script.
You don't have to specify a variable as well so you wouldn't need it. read uses default variable $REPLY.
This question already has answers here:
Timeout a command in bash without unnecessary delay
(24 answers)
Closed 8 years ago.
I want to make my script wait for n seconds, during which user can abort the script. My code till now looks like this
echo "Start Date provided :" $STARTDATE
echo "End date provided :" $ENDDATE
echo ""
echo "Please check the dates provided and in case of error press CTRL+C"
seq 1 15 |while read i; do echo -ne "\rWaiting for $i seconds"; sleep 1; done
echo ""
echo "Executing query on the remote side, please wait..."
When the user presses ctrl+c though what happens is that the while loop ends and continues executing the rest of the script.
How can I make it abort the entire script? Thanks in advance for any advise
I would use trap
#!/bin/bash
trap 'echo -e "\nBye"; exit 1' INT
echo "Please check the dates provided and in case of error press CTRL+C"
seq 1 15 |while read i; do echo -ne "\rWaiting for $i seconds"; sleep 1; done
echo "Executing query on the remote side, please wait..."
Here's a simpler program that reproduces your problem:
true | sleep 10 # Hit Ctrl-C here
echo "why does this execute?"
The problem is that bash determines whether or not to continue the script based on whether the SIGINT was handled. It determines whether the SIGINT was handled based on whether or not the process it's currently running is killed by this signal.
Since seq prints its numbers and exits immediately and successfully, it's not killed by SIGINT. bash therefore incorrectly assumes the signal was handled, and it keeps going.
You can solve this in two ways:
Add a SIGINT handler that exits your script regardless of whether the signal appears to be handled.
Don't use seq or anything else that immediately exits.
For the first approach, see user3620917's answer.
The second approach,
for i in {15..1}
do
printf '\rYou have %d seconds to hit Ctrl-C' "$i"
sleep 1
done
To catch CTRL+C you can use trap. For example:
#!/bin/bash
trap 'got_one' 2
got_one() {
echo "I got one"
exit 69
}
echo -e "PID: $$, PPID: $PPID"
sleep 100
So your script can look like this:
#!/bin/bash
trap "interrupt" SIGINT
interrupt() {
echo -e "\nExecution canceled."
exit 69
}
countdown() {
duration=$1 # in seconds
seq $duration -1 0|while read i; do echo -ne "\rWaiting for $i seconds"; sleep 1; done
}
STARTDATE="foo"
ENDDATE="bar"
cat <<END
Start Date provided: $STARTDATE
End date provided: $ENDDATE
Please check the dates provided and in case of error press CTRL+C
END
countdown 15
echo -e "\nExecuting query on the remote side, please wait..."
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