终端 UI 与进度条:ANSI 控制码

系统讲解 ANSI 转义序列控制颜色与光标、用回车与 tput 实现动态进度条、交互式输入与菜单选择,以及信号捕获和终端状态恢复的工程要点。

1. ANSI 转义序列基础

一句话总结: 终端通过 ANSI 转义序列(\033[ 开头)控制颜色、光标与屏幕,脚本据此画出动态界面。

终端本质是「接收字节流的渲染器」,特定字节序列不是字符而是指令。

# 最基础:\033[31m 红色,\033[0m 重置
echo -e "\033[31m红色文字\033[0m 正常文字"

# 用 printf 更清晰
printf '\033[32m绿色\033[0m\n'
printf '\033[1;34m粗体蓝色\033[0m\n'

1.1 颜色表速查

前景色 3037,背景色 4047,加粗 1,下划线 4,重置 0。

# 前景色
for c in 30 31 32 33 34 35 36 37; do
  printf '\033[%sm%s\033[0m ' "$c" "$c"
done
echo

# 背景色
for c in 40 41 42 43 44 45 46 47; do
  printf '\033[%sm%s\033[0m ' "$c" "$c"
done
echo

# 组合样式
printf '\033[1;31;43m加粗红字黄底\033[0m\n'

一句话总结: 记忆规律是「3x 前景 / 4x 背景 / 1 加粗」,结尾务必 \033[0m 重置,否则颜色泄漏到后续所有输出。

1.2 256 色与真彩色

# 256 色: \033[38;5;N m
printf '\033[38;5;196m256色红\033[0m\n'

# 真彩色: \033[38;2;R;G;B m
printf '\033[38;2;255;128;0m橙橙色\033[0m\n'

# 检测终端颜色支持
echo "$TERM"
[ "$TERM" = "xterm-256color" ] && echo "支持 256 色"

2. 光标控制与单行刷新

一句话总结: 光标移动序列让脚本能在同一位置反复绘制,\r 回车是「回到行首重写」实现进度条的最小前提。

# \r 回车 + 不换行:重写当前行
printf '进度 10%%\r'
sleep 1
printf '进度 50%%\r'
sleep 1
printf '进度 100%%\n'

# 光标上移一行: \033[1A
printf '第一行\n'
printf '\033[1A重写第一行\033[0K\n'

# 清行: \033[2K 清除整行, \033[0K 清除到行尾
printf '残留内容\033[2K新内容\n'

2.1 隐藏光标与恢复

# 隐藏光标: \033[?25l, 恢复: \033[?25h
printf '\033[?25l隐藏光标\n'
sleep 1
printf '\033[?25h恢复光标\n'

一句话总结: \r 重写单行、\033[1A 上移多行、\033[2K 清行、\033[?25l 藏光标——四个序列拼出一切动态界面。

2.2 tput 可移植写法

tput 根据 terminfo 生成正确序列,比硬编码转义更可移植。

# tput 等价写法
tput setaf 2; echo "绿色"; tput sgr0
tput cup 3 10; echo "第3行第10列"

# 常用 tput 命令
bold=$(tput bold)
red=$(tput setaf 1)
reset=$(tput sgr0)
echo "${bold}${red}告警${reset}"

3. 进度条实现

一句话总结: 进度条 = 循环里用 \r 重写一行,按完成比例绘制「百分比 + 填充块」,配合真实任务计算进度。

#!/usr/bin/env bash
# 最简进度条
total=100
for ((i=0; i<=total; i+=10)); do
  printf '进度: %3d%%\r' "$i"
  sleep 0.2
done
echo "进度: 100%"

3.1 带填充块的进度条

#!/usr/bin/env bash
progress() {
  local total="$1" cur="$2"
  local width=40
  local filled=$((cur * width / total))
  local bar=""
  for ((i=0; i<width; i++)); do
    [ "$i" -lt "$filled" ] && bar+="#" || bar+="-"
  done
  printf '[%s] %3d%%\r' "$bar" "$((cur * 100 / total))"
}

for ((i=0; i<=20; i++)); do
  progress 20 "$i"
  sleep 0.1
done
echo ""

一句话总结: 填充块数量 = 进度 / 总数 * 宽度,用整数运算避免浮点,\r 让进度条停留在同一行。

3.2 带真实任务的进度

#!/usr/bin/env bash
set -euo pipefail
# 模拟下载:按文件数推进进度
files=(a.zip b.zip c.zip d.zip e.zip)
total=${#files[@]}
cur=0
for f in "${files[@]}"; do
  # 真实任务
  sleep 0.5
  ((cur+=1))
  printf '下载 %s... [%-20s] %3d%%\r' \
    "$f" "$(printf '#%.0s' $(seq 1 $((cur * 20 / total))))" \
    $((cur * 100 / total))
done
echo ""

4. 交互式输入与菜单

一句话总结: read -p 带提示、read -s 隐藏输入、read -n 单键读取,三个组合实现交互式菜单与密码输入。

#!/usr/bin/env bash
# 带提示的读取
read -r -p "请输入名字: " name
echo "你好, $name"

# 隐藏输入(密码)
read -r -s -p "密码: " pass
echo ""
echo "密码长度: ${#pass}"

4.1 单键菜单选择

#!/usr/bin/env bash
echo "请选择操作:"
echo "  [1] 构建"
echo "  [2] 测试"
echo "  [3] 部署"
read -r -n 1 -p "> " choice
echo ""
case "$choice" in
  1) echo "执行构建" ;;
  2) echo "执行测试" ;;
  3) echo "执行部署" ;;
  *) echo "无效选择: $choice" ;;
esac

一句话总结: read -n 1 读单键不用回车、read -s 隐藏回显、read -r 防反斜杠转义,三件套覆盖交互输入。

4.2 带默认值的输入

#!/usr/bin/env bash
read -r -p "端口 [8080]: " port
port="${port:-8080}"
echo "使用端口: $port"

# 超时控制
if read -r -t 5 -p "5秒内确认 (y/N): " ans; then
  [[ "$ans" =~ ^[Yy]$ ]] && echo "已确认" || echo "已取消"
else
  echo "超时, 默认取消"
fi

5. 信号捕获与终端状态恢复

一句话总结: 动态界面脚本被 Ctrl+C 中断时可能留下隐藏光标、关闭的回显,必须用 trap 恢复终端状态。

#!/usr/bin/env bash
# 记录原始状态并在退出时恢复
save_state() {
  stty_save=$(stty -g)
  trap 'tput cnorm; stty "$stty_save"; exit 1' INT TERM EXIT
  stty -echo   # 关闭回显
  tput civis   # 隐藏光标
  # ... 动态界面逻辑 ...
  tput cnorm   # 恢复光标
  stty "$stty_save"
}
save_state

5.1 trap 清理的标准写法

#!/usr/bin/env bash
set -euo pipefail

cleanup() {
  tput cnorm            # 恢复光标
  stty echo             # 恢复回显
  echo ""               # 换行避免残留
}
trap cleanup EXIT INT TERM

# 隐藏光标并进入原始模式
tput civis
stty -echo
# 动态任务
for i in $(seq 1 20); do
  printf '工作中 %2d/20\r' "$i"
  sleep 0.1
done
# 退出时自动 cleanup

一句话总结: trap cleanup EXIT INT TERM 让「退出即恢复终端」,stty -echo 与 tput civis 成对出现、成对恢复。

6. 多行动态输出

一句话总结: 用 \033[NA 上移 N 行再重写,可以维护「多行状态区」,比如同时展示多个任务的进度。

#!/usr/bin/env bash
# 多行进度区: 每次重写后光标回到顶部
tasks=(任务A 任务B 任务C)
lines=${#tasks[@]}

for round in $(seq 1 5); do
  for t in "${tasks[@]}"; do
    printf '%s: 第 %d 轮\r' "$t" "$round"
    printf '\033[1B'   # 下移一行
  done
  printf '\033[%dA' "$lines"   # 回到顶部
  sleep 0.3
done
echo ""

6.1 tput 维护整屏

#!/usr/bin/env bash
# 清屏并重绘
tput clear
tput cup 1 2; echo "标题"
tput cup 3 2; echo "状态: 运行中"
tput cup 5 2; echo "按 q 退出"

# 读取屏幕尺寸
echo "行数: $(tput lines), 列数: $(tput cols)"

一句话总结: tput clear 清屏、tput cup 行 列 定位,适合画「整屏仪表盘」,配合循环定时重绘。

6.2 彩色状态徽章

#!/usr/bin/env bash
ok="\033[32m✔\033[0m"
fail="\033[31m✘\033[0m"
printf "检查配置 %b\n" "$ok"
printf "检查依赖 %b\n" "$ok"
printf "检查权限 %b\n" "$fail"

7. 实战:带 UI 的备份脚本

一句话总结: 把颜色、进度条、交互确认、信号清理组合成「生产级外观」的脚本,让终端自动化既好看又可中断。

7.1 完整示例

#!/usr/bin/env bash
set -euo pipefail

# 颜色与工具
c_green=$(tput setaf 2)
c_red=$(tput setaf 1)
c_yellow=$(tput setaf 3)
c_reset=$(tput sgr0)

cleanup() { tput cnorm; echo ""; }
trap cleanup EXIT INT TERM

log() { printf '%s[%s]%s %s\n' "$c_green" "$(date +%T)" "$c_reset" "$*"; }
warn() { printf '%s[警告]%s %s\n' "$c_yellow" "$c_reset" "$*"; }
die()  { printf '%s[错误]%s %s\n' "$c_red" "$c_reset" "$*" >&2; exit 1; }

tput civis
log "开始备份 /data → /backup"
for f in /data/*; do
  cp -r "$f" /backup/ || warn "跳过 $f"
  printf '已处理: %s\r' "$(basename "$f")"
done
echo ""
log "备份完成"

一句话总结: 抽象 log/warn/die 三个着色函数 + trap cleanup,动态脚本的「外观」与「安全」同时到位。

7.2 进度 + 交互确认

#!/usr/bin/env bash
set -euo pipefail
trap 'tput cnorm; exit 1' INT TERM

read -r -p "确认清理 3 天前的日志? [y/N] " ans
[[ "$ans" =~ ^[Yy]$ ]] || exit 0

find /var/log -type f -mtime +3 -print0 \
  | xargs -0 -r rm -f
echo "清理完成"

7.3 跨平台终端差异

#!/usr/bin/env bash
# 非终端环境(管道/CI)禁用颜色
if [ ! -t 1 ]; then
  c_green=""; c_red=""; c_yellow=""; c_reset=""
fi
echo "${c_green}${c_reset}输出到文件时无颜色代码"

# 检测终端类型
case "$TERM" in
  *256color*) echo "支持256色" ;;
  dumb) echo "极简终端" ;;
  *) echo "基础终端" ;;
esac

8. 总结

环节要点
转义序列\033[ 开头,控制颜色/光标/屏幕
颜色3x 前景、4x 背景、1 加粗,结尾重置
单行刷新\r 重写 + \033[2K 清行
进度条循环内 \r 重绘,整数运算算填充
交互输入read -s 隐藏、-n 1 单键、-t 超时
信号清理trap cleanup 恢复光标与回显
多行 UI\033[NA 上移重绘,tput 可移植
生产原则非终端禁用颜色、函数化封装日志

终端 UI 让脚本从「黑箱运行」变成「可见、可控、可中断」:ANSI 控制码画界面,trap 保证中断不留残迹,-t 1 检测让输出在管道与 CI 里依旧干净。这套能力在部署发布脚本里特别有用——最后进入部署与发布脚本专题收尾。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「shell」更多文章

  1. 任务编排与 Makefile 实战
  2. 文件监控与事件驱动流水线实战
  3. 结构化数据清洗与报表生成实战