Mystery0の小站

Mystery0の小站

Bash Shell 小棍进度条开发

Bash Shell 小棍进度条开发

前言

这两天在给公司开发 goreply 管理脚本,就写了好几天的 Shell 脚本,今天上午终于交付给了QA,最后有个QA提出了几点建议,其中一点就是在处理大量的文件的时候(目前我使用的遍历,所以在演示操作的时候卡住了),输出一个进度条,提示用户是正在处理,而不是出现了异常情况导致卡住。

所以我就打开了前段时间收藏的一篇文章,在这里做一下简单的记录。

因为原始代码并非我编写的,所以在这里只是简单讲一下该文章的思路以及对最后的脚本做一点更改。

原文链接

bash耗时命令进度条

原理讲解

因为我们只需要一个简易的进度条即可,所以一个最简单的进度条就像 Windows XP 时代我们执行一个耗时的操作,鼠标会变成一个沙漏⌛️然后不停地旋转。精简到 Shell 上的话,就是一个小棍子,不考虑其他骚操作的情况下,我们只需要定义几个状态,即 / | \ - 。

然后在特定光标位置一直循环输出这四个字符就行了,于是我们很容易就写出了以下代码:

while :; do # 无限循环
    for j in '-' '\\' '|' '/'; do
      tput sc # 保存当前光标所在位置
      echo -ne "$j" # 输出这一秒展示的字符
      sleep 1 # 每一秒钟更新一次
      tput rc # 恢复光标到最后保存的位置
    done
  done

通过一个无限循环的操作,每一秒钟更新指定光标位置的字符,实现动态的小棍子 。

问题

上面代码我们只能得到一根无限循环的 小棍子 ,但是我们经常是执行一个耗时操作,然后在开始执行的时候输出这个 小棍子 ,然后在执行完毕的时候取消展示 小棍子 ,改为输出正常的数据,那么肯定不用直接使用上面的代码来阻塞执行,所以需要 输出 小棍子 的进程和 执行耗时操作 的进程并发执行,当 执行耗时操作 完成时,关掉输出 小棍子 的进程。 所以我们稍加思索,写出如下代码:

# 输出进度条, 小棍型
procing() {
  trap 'exit 0;' 6 # 接收耗时操作执行完毕的信号,用来退出循环
  while :; do # 无限循环
    for j in '-' '\\' '|' '/'; do
      tput sc # 保存当前光标所在位置
      echo -ne "$j" # 输出这一秒展示的字符
      sleep 1 # 每一秒钟更新一次
      tput rc # 恢复光标到最后保存的位置
    done
  done
}

# 等待执行完成
waiting() {
  local pid="$1"
  procing & # 后台执行输出小棍子的进程
  local tmppid="$!" # 获取小棍子进程的pid,用于后续终止
  wait "$pid" # 等待耗时操作执行完成
  tput rc # 恢复光标到最后保存的位置,替代小棍子
  kill -6 $tmppid >/dev/null 1>&2 # 终止小棍子进程
}

# 执行某些耗时操作
do_something_background() {
  echo -e "$2" # 打印执行耗时操作之前的信息文本
  eval "$1" & # 根据第一个参数执行操作
  waiting "$!" # 等待耗时操作执行
}

使用

do_something_background 'sleep 10' "这里是日志..."
echo -e "执行成功!"

运行结果

shell_progress_1

shell_progress_2

shell_progress_3

结语

上面的代码已经是能够使用的了,大部分代码和思路取自原作者,在这里我只是做了一下简单的优化。

更新

# 输出进度条 横向
print_progress_bar() {
  local progress=$1
  local extra_message=$2
  local show_str=''
  local progress_cols=$((${COLUMNS} - 17))
  # 计算进度条最大宽度
  local max_length=100
  if [[ $max_length -gt $progress_cols ]]; then
    max_length=$progress_cols
  fi
  local split=100/$max_length
  local i=0
  while [[ $i -le $max_length ]]; do
    # 计算当前进度
    split_progress=$i*$split
    next_progress=$split_progress+$split
    if [[ $progress -ge $next_progress ]]; then
      # 进度大于当前进度,填充=
      show_str+="="
    elif [[ $progress -le $split_progress ]]; then
      # 进度小于当前进度,填充空白
      break
    else
      # 进度大于当前进度,小于下一个进度,填充-
      show_str+="-"
    fi
    ((i++))
  done
  printf "[%-${max_length}s][%d%%]%s\r" "$show_str" "$progress" "$extra_message"
}

# 输出进度条, 小棍型
procing() {
  trap 'exit 0;' 6 # 接收耗时操作执行完毕的信号,用来退出循环
  little_stick=('-' '\' '|' '/')
  little_stick_size=${#little_stick[*]}
  ellipsis=('.' '..' '...' '....' '.....' '......')
  ellipsis_size=${#ellipsis[*]}
  procing_index=0
  while :; do # 无限循环
    tput sc
    tput el
    little_stick_index=$procing_index%$little_stick_size
    ellipsis_index=$procing_index%$ellipsis_size
    printf "%s    %-6s    %s" "${little_stick[$little_stick_index]}" "${ellipsis[$ellipsis_index]}" "${little_stick[$little_stick_index]}"
    sleep 0.5 # 每一秒钟更新一次
    tput rc
    ((procing_index++))
  done
}

# 等待执行完成
waiting() {
  local pid="$1"
  procing &# 后台执行输出小棍子的进程
  local tmppid="$!"               # 获取小棍子进程的pid,用于后续终止
  wait "$pid"                     # 等待耗时操作执行完成
  tput rc                         # 恢复光标到最后保存的位置,替代小棍子
  kill -6 $tmppid >/dev/null 1>&2 # 终止小棍子进程
}

# 执行某些耗时操作
do_something_background() {
  echo -ne "$2  " # 打印执行耗时操作之前的信息文本
  tput civis
  eval "$1" &# 根据第一个参数执行操作
  waiting "$!" # 等待耗时操作执行
  tput cnorm
  tput el
  echo
}

更新代码运行结果

Snipaste_2020-02-29_19-31-29

Snipaste_2020-02-29_19-31-42

参考链接

bash耗时命令进度条

tput 入门

Shell printf 命令