R99Photography
R99Photography

Reputation: 81

Bash Script: Pause a countdown and resume it by pressing ENTER (or any)

I have a task scheduler which runs a bash script. The task first opens a GIT Bash terminal, an opening message is shown ("The script is about to start in 60 seconds.") and runs a script at the end of that countdown.

Now, I would like to improve user experience, allowing him/her to stop/resume the countdown or (without any intervention) leaving the script running automatically. So, this is the procedure flow:

  1. After the GIT Bash terminal open, allow the user to pause the script within the time frame shown by pressing ENTER or any other key;
  2. If no action taken by user, the countdown continues and the script will run at the end of the timeframe;
  3. If the user had pressed ENTER (or any other), then by pressing again ENTER (or any other key) he/she resumes the countdown and the will run immediately.

I've tried to use the read -p but it is not good for me: I don't want the user action to fire something but to stop/pause the countdown instead (and then resume it).

Upvotes: 2

Views: 1188

Answers (1)

Andrey Tyukin
Andrey Tyukin

Reputation: 44957

Update history:

  • A Pausable Countdown is implemented in the first part of the answer (prints lot of lines)
  • A much less verbose Pausable Timeout is implemented in the second part (prints one static line + additional messages on key press)
  • A somewhat more sophisticated Pausable countdown that constantly updates the same line is in the third code snippet.

Pausable Countdown

Combining some hints from similar questions here and some external resources about how to read single character (e.g. here, otherwise everywhere on the internet), and adding an additional loop for resumption, this is what I came up with:

#!/bin/bash

# Starts a pausable/resumable countdown.
# 
# Starts the countdown that runs for the
# specified number of seconds. The 
# countdown can be paused and resumed by pressing the
# spacebar. 
#
# The countdown can be sped up by holding down any button
# that is no the space bar.
#
# Expects the number of seconds as single
# argument.
#
# @param $1 number of seconds for the countdown
function resumableCountdown() {
  local totalSeconds=$1
  while (( $totalSeconds > 0 ))
  do
    IFS= read -n1 -t 1 -p "Countdown $totalSeconds seconds (press <Space> to pause)" userKey
    echo ""
    if [ "$userKey" == " " ]
    then
      userKey=not_space
      while [ "$userKey" != " " ]
      do
        IFS= read -n1 -p "Paused, $totalSeconds seconds left (press <Space> to resume)" userKey
    echo ""
      done
    elif  [ -n "$userKey" ]
    then
      echo "You pressed '$userKey', press <Space> to pause!"
    fi
    totalSeconds=$((totalSeconds - 1))
  done
}

# little test
resumableCountdown 60

This can be saved and run as a stand-alone script. The function can be reused elsewhere. It pauses / resumes with SPACE, because this seemed to be more intuitive to me, because it's how it works e.g. in video-players embedded in browsers.

The countdown can also be sped up by pressing keys other than the space bar (that's a feature).


Issuing a warning message and waiting for a pausable timeout

The following variation implements a pausable timeout, which prints nothing but the initial warning message, unless the user pauses or resumes the (internal) countdown by pressing the spacebar:

# Prints a warning and then waits for a
# timeout. The timeout is pausable.
#
# If the user presses the spacebar, the 
# internal countdown for the timeout is 
# paused. It can be resumed by pressing
# spacebar once again.
#
# @param $1 timeout in seconds
# @param $2 warning message
warningWithPausableTimeout() {
  local remainingSeconds="$1"
  local warningMessage="$2"
  echo -n "$warningMessage $remainingSeconds seconds (Press <SPACE> to pause)"
  while (( "$remainingSeconds" > 0 ))
  do
    readStartSeconds="$SECONDS"
    pressedKey=""
    IFS= read -n1 -t "$remainingSeconds" pressedKey
    nowSeconds="$SECONDS"
    readSeconds=$(( nowSeconds - readStartSeconds ))
    remainingSeconds=$(( remainingSeconds - readSeconds ))
    if [ "$pressedKey" == " " ]
    then
      echo ""
      echo -n "Paused ($remainingSeconds seconds remaining, press <SPACE> to resume)"
      pressedKey=""
      while [ "$pressedKey" != " " ]
      do
        IFS= read -n1 pressedKey
      done
      echo ""
      echo "Resumed"
    fi
  done
  echo ""
}

warningWithPausableTimeout 10 "Program will end in"
echo "end."

Pausable countdown that updates the same line

This is a countdown similar to the first one, but it takes only a single line. Relies on echo -e for erasing and overriding previously printed messages.

# A pausable countdown that repeatedly updates the same line.
#
# Repeatedly prints the message, the remaining time, and the state of
# the countdown, overriding the previously printed messages.
#
# @param $1 number of seconds for the countdown
# @param $2 message
singleLinePausableCountdown() {
  local remainingSeconds="$1"
  local message="$2"
  local state="run"
  local stateMessage=""
  local pressedKey=""
  while (( $remainingSeconds > 0 ))
  do
    if [ "$state" == "run" ]
    then
      stateMessage="[$remainingSeconds sec] Running, press <SPACE> to pause"
    else
      stateMessage="[$remainingSeconds sec] Paused, press <SPACE> to continue"
    fi
    echo -n "$message $stateMessage"
    pressedKey=""
    if [ "$state" == "run" ]
    then 
      IFS= read -n1 -t 1 pressedKey
      if [ "$pressedKey" == " " ]
      then
        state="pause"
      else 
        remainingSeconds=$(( remainingSeconds - 1 ))
      fi
    else
      IFS= read -n1 pressedKey
      if [ "$pressedKey" == " " ]
      then
        state="run"
      fi
    fi
    echo -ne "\033[1K\r"
  done
  echo "$message [Done]"
}

This one might behave strangely if the line is longer than the console width (it does not erase the line completely).


Unsorted collection of hints for anyone who tries to make sth. similar:

  • IFS= read -n1 reads single character
  • read -t <seconds> sets time-out for read. Once the timeout expires, read exits with non-zero, and sets variable to empty.
  • Magic bash built-in variable $SECONDS measures the time from the start of the script in seconds.
  • If a line has been printed with echo -n, then it can be erased and reset with echo -ne "\033[1K\r".

Upvotes: 4

Related Questions