【问题标题】:Bash Script: Pause a countdown and resume it by pressing ENTER (or any)Bash 脚本:暂停倒计时并按 ENTER(或任意键)恢复
【发布时间】:2018-02-12 22:41:15
【问题描述】:

我有一个运行 bash 脚本的任务调度程序。该任务首先打开一个 GIT Bash 终端,显示一条打开消息(“脚本将在 60 秒后开始。”)并在倒计时结束时运行一个脚本。

现在,我想改善用户体验,允许他/她停止/恢复倒计时或(无需任何干预)让脚本自动运行。所以,这是程序流程:

  1. GIT Bash 终端打开后,允许用户在按 ENTER 或任何其他键显示的时间范围内暂停脚本;
  2. 如果用户未采取任何操作,则倒计时继续,脚本将在时间范围结束时运行;
  3. 如果用户按了 ENTER(或任何其他键),然后再次按 ENTER(或任何其他键),他/她将恢复倒计时并立即运行。

我尝试使用read -p,但这对我不利:我不希望用户操作触发某些东西,而是停止/暂停倒计时(然后恢复)。

【问题讨论】:

    标签: bash shell scheduled-tasks git-bash taskscheduler


    【解决方案1】:

    更新历史:

    • 答案的第一部分实现了Pausable Countdown(打印很多行)
    • 第二部分实现了一个不太冗长的Pausable Timeout(打印一个静态行+按键时的附加消息)
    • 第三个代码 sn-p 中有一个更复杂的可暂停倒计时,不断更新同一行

    可暂停倒计时

    结合类似问题here的一些提示和一些关于如何读取单个字符(e.g. here, otherwise everywhere on the internet)的外部资源,并添加一个额外的循环用于恢复,这就是我想出的:

    #!/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
    

    这可以作为独立脚本保存和运行。该功能可以在其他地方重用。它使用SPACE 暂停/恢复,因为这对我来说似乎更直观,因为它就是这样工作的,例如在嵌入浏览器的视频播放器中。

    还可以通过按空格键以外的键来加快倒计时(这是一项功能)。


    发出警告消息并等待可暂停的超时

    以下变体实现了一个可暂停超时,它只打印初始警告消息,除非用户通过按空格键暂停或恢复(内部)倒计时:

    # 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."
    

    更新同一行的可暂停倒计时

    这是一个倒计时,类似于第一个,但它只需要一行。依靠echo -e 擦除和覆盖以前打印的消息。

    # 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]"
    }
    

    如果线条长于控制台宽度(它不会完全擦除线条),这可能会表现得很奇怪。


    未分类的提示集合,供任何尝试做某事的人使用。类似的:

    • IFS= read -n1 读取单个字符
    • read -t &lt;seconds&gt;read 设置超时。一旦超时到期,read 会以非零值退出,并将变量设置为空。
    • Magic bash 内置变量$SECONDS 以秒为单位测量从脚本开始的时间。
    • 如果一行已经用echo -n 打印,那么它可以用echo -ne "\033[1K\r" 擦除和重置。

    【讨论】:

    • 有什么理由[ ! -z "$userKey" ] 不能是[ -n "$userKey" ]?还是[ ! "$userKey" == " " ][ "$userKey" != " " ]? (nit:字符串相等只有一个=
    • @DavidC.Rankin 感谢您的提示,将[ ! -z 替换为[ -n。没有什么好理由,我只是记得-z更好,不记得-n。还合并了一些嵌套的ifs,它们是不必要的。谢谢。
    • @DavidC.Rankin 还修复了繁琐的字符串比较。当我在做的时候,我可能最好去看看记录 bash 函数的正确语法是什么......再次感谢你,非常有帮助;)
    • Bash 在 name ()function name [()] 的函数定义上相当灵活,因此除了标准的 name () 之外,您还可以使用 function namefunction name ()
    • 它不能按需要工作。脚本每秒都会回显“倒计时 XX 秒...”,我不希望这样,我希望在终端启动时出现一条初始消息,告知用户在接下来的 60 秒内会发生一些事情。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-08
    • 1970-01-01
    • 2017-02-15
    • 1970-01-01
    相关资源
    最近更新 更多