基于whiptail开发shell导航工具

本文结合实际工程经验,系统总结 如何用 whiptail 设计一个"可回退"的交互式界面框架,以及几个经常踩的坑。

一、whiptail 是什么?

whiptail 是一个TUI(Terminal UI)工具,它受 dialog (诞生于1990年代)启发、但更加轻量,功能更精简的"兼容替代品",方便开发人员开发出dialog 风格交互UI界面。由redhat开发。他们的区别如下:

对比项 dialog whiptail
底层库 ncurses newt
设计目标 功能完整 轻量、简单
样式控制 ✅ 支持颜色 / 粗体 ❌ 不支持
可扩展性 很强 很弱
兼容性 POSIX 广 基本兼容 dialog 参数
学习曲线 稍高 很低

二、whiptail 基本语法

参考博客:Linux命令------whiptail交互式shell脚本对话框了解基本的语法。

博客介绍了消息框,yes/no对话框,表单输入框,密码输入框,菜单栏,radiolist对话框,多选对话框,进度条等等功能。我目前只使用了 消息框,yes/no对话框 和单选菜单栏。

三、实现可回退的交互界面

3.1 了解shell基础知识

3.1.1 了解stdin, stdout, stderr退出码

退出码是 Shell 脚本实现自动化逻辑判断 的基石。退出码不会被输出到stderrstdout,只会默认流向 Shell 解释器的内部寄存器(即特殊变量 $?)。你可以手动通过echo $?来查看退出码。父进程会检查子进程的退出码来判断其是否成功。

一般的shell脚本都会增加set -e来让进程检查子进程的退出码是否为0,如果不为0立即退出脚本,确保脚本安全性。

3.1.2 $(...)只返回stdout

先说结论$(...)只返回子进程中的stdout,子进程的stderr会被直接显示到当前shell的stderr(也就是屏幕上)。

用下面的例子可很好的证明 $(...)只返回stdout

shell 复制代码
#!/bin/bash
choice=$(echo "12345" 1>&2)
echo "Captured choice: $choice"

输出为:

复制代码
$ ./test.sh
12345
Captured choice:

$(...)创建了一个子进程,子进程完全复制母进程的 FD 表,即继承母 shell 的文件描述符(stdout / stderr)。1>&2则把子进程内原本输出到stdout的内容重定向到了stderr,结果直接显示到了当前shell的屏幕上(stdout自然就空了所以choice被赋空值)。

下面我们稍微修改,去掉 1>&2,如下代码:

shell 复制代码
#!/bin/bash
choice=$(echo "12345")
echo "Captured choice: $choice"

输出为:

复制代码
$ ./test.sh
Captured choice: 12345
3.1.3 if result=$(...) then;语句中if在判断什么?

if用于检查退出码是否为0,而赋值语句的退出码 = 内部命令的退出码,也就是说这段代码在判断$(...)中命令退出码是多少。不在乎result的值是多少,不在乎赋值操作是否成功,只在乎退出码

3.1.4 如何理解3>&1

但在 X>&Y 的语法中,箭头 > 的方向决定了数据的"流向"定义权,而 &Y 是"源头"

  • 语法结构目标描述符 > 源描述符的引用
  • 3>&1
    • 左边 (3) :是接收者。我们要设置 fd 3,是我们要操作的目标。
    • fd1是 "通往屏幕的管子",拿一根新管子(fd 3),把它接成和 fd 1 一模一样(通往屏幕),让 fd 3 指向 fd 1 去的地方,结果是fd 3 和 fd 1 指向同一个地方(通常是屏幕)。
  • 如果写成**3>1**,没了&,1会被识别成文件名,含义变成了打开(或创建)一个名字叫 1 的普通文件,并将文件描述符 3 指向这个文件。

3.2 如何实现可回退的多界面?

因为多界面我们选择回退信号为用户按下cancel键后(退出码为1)进行回退,所以我们需要判断这个信号。因为set -t需要捕获退出码,whiptail 在退出码为1时不是出错了,所以为了实现回退必须要把whiptail放到判断语句中。为了获取用户输入返回值,需要将whiptail放到$(...)中。而结合whiptail的输出实际情况和$(...)只捕获stdout的实际情况,需要考虑使用 3>&1 1>&2 2>&3才能让UI正确显示,让用户选项被输入到$(...)返回。

多个界面就是单个界面的组合。每个界面封装成一个函数。函数被维护在一个关系数组中形成一个状态机,STEP为关系数组的系数。主函数main中通过while循环判断当前为哪个STEP。在每个状态内部完成STEP切换。

3.2.1 增加set -t后的坑

如果shell脚本增加set -e来检查个个命令的退出码确保安全性,检测到退出码不为0直接退出脚本,防止脚本在错误的路上狂奔。而增加了 whiptail 命令会因选择Cancel后返回1而误认为异常退出。咋办呢?

set -e 确实会检查退出码,但它有一个"豁免规则":如果命令的退出码已经被用于逻辑判断(比如在 if 中),set -e 就会认为这个错误"已经被处理了",从而选择忽略它。

所以我们将 whiptail 命令放到判断语句中就行拉。

3.2.2 whiptail 的输出和退出码

先说结论

whiptail 输出到
UI界面 stdout
用户选择的菜单 stderr
退出码 Yes或其他菜单则为 0 Cancel 则为1(不是异常) 出现错误为其他大于1的值(是异常)

通过执行下面两个不同的脚本:

shell 复制代码
whiptail --title "Menu test" \
                  --menu "Choose one" \
                  12 50 2 \
                  AAA "Option A" \
                  BBB "Option B" \
		         1>/dev/null
		         
whiptail --title "Menu test" \
                  --menu "Choose one" \
                  12 50 2 \
                  AAA "Option A" \
                  BBB "Option B" \
		         2>/dev/null

你可以明显发现,第1个没输出,第2个有输出,所以证明**whiptail的UI界面输出到了 stdout**(文件描述符为1)。但是你想要得到用户反馈就需要用上$(...)了。

然后再实验:

shell 复制代码
whiptail --title "Menu test" \
                  --menu "Choose one" \
                  12 50 2 \
                  AAA "Option A" \
                  BBB "Option B" \
		         2>err.txt

当选择不同的选项时,这个选项就会被写入err.txt文件(文件内容为 AAA 或 BBB)。所以证明用户选项被输出到了stderr(文件描述符为2)。

退出码 ( $ ?)很简单就知道了,就不放例子了:

  • 情况 A(用户点了"确定")whiptail 返回退出码 0
  • 情况 B(用户点了"取消"或按了 ESC)whiptail 返回退出码 1.
3.2.3 经常看到 3>&1 1>&2 2>&3

在shell脚本中我们通常会写:choice=$(whiptail ... 3>&1 1>&2 2>&3),我们知道$(...) 返回的是命令在 stdout (文件描述符 1)上产生的字节流。但是前面我们讨论了,这个子进程中运行的whiptail的UI屏幕默认输出到子进程的 stdout(fd 1),用户选项 默认输出到子进程的 stderr(fd 2)。我们为了让choice得到用户选项 ,就需要在子进程内实现stdout(fd 1)和 stderr(fd 2)的交换。但不能直接交换。

!WARNING

不能直接交换为啥?

不能直接交换(例如写成 1>&2 2>&1)的原因在于:Shell 的重定向是按顺序从左到右执行的,且直接覆盖会导致原始目标丢失。 3>&1 1>&2 2>&3 这段看似晦涩的代码,实际上是在说:

"先把原本的屏幕(fd 1)藏到 fd 3 里;然后把选择输出(fd 2)挪到 fd 1 给变量捕获;最后把藏起来的屏幕(fd 3)挪给 fd 2 去显示界面。"

这就是 Shell 脚本中经典的"三杯交换法"。

3.2.4 代码框架
shell 复制代码
# Declare associative array for route table
declare -A ROUTES=(
    [1]=step_state1
    [2]=step_state2
    [3]=step_state3
    ......
)
......
# Step 1: Initial 
step_state1() {
    if whiptail --title "Step 1: Preparation Check" --backtitle "$BACK_TITLE" \
        --no-button "Back" \
        --yesno "Continue?" \
        14 60; then
        if storage_device_detection; then
            STEP=$((STEP+1))
        else
            STEP=0
        fi
    else
        STEP=0
    fi
}

# Step 2: xxx
step_state2() {
    local choice
    if choice=$(whiptail --title "Step 2: Select Board" --backtitle "$BACK_TITLE" \
        --cancel-button "Back" \
	    --menu "Different versions support different board types.):" \
        14 60 4 \
        "board1" ": some comments" \
        "board2" ": some comments" \
        "board3" ": some comments" \
        3>&1 1>&2 2>&3
    ); then
        BOARD="$choice" # 更新全局变量
        STEP=$((STEP+1))
    else
        STEP=$((STEP-1))
    fi
}

# Step 3: xxx
step_state3() {
    local choice
    if choice=$(whiptail --title "Step 3: Select sdk" --backtitle "$BACK_TITLE" \
        --cancel-button "Back" \
	    --menu "SDK lists:" \
        14 60 4 \
        "SDK1" ": some comments" \
        "SDK2" ": some comments" \
        3>&1 1>&2 2>&3
    ); then
        SDK_VERSION="$choice" # 更新全局变量
        STEP=$((STEP+1))
    else
        STEP=$((STEP-1))
    fi
}

main() {
    while true; do
        # Validate STEP is within valid range
        if [ -z "${ROUTES[$STEP]}" ]; then
            if [ "$STEP" -eq 0 ]; then  # STEP是全局变量,为0退出
                exit 0
            else
                whiptail --title "Error" --backtitle "$BACK_TITLE" --msgbox \
                    "Invalid step number: $STEP" \
                    10 50
                exit 1
            fi
        fi

        # Execute the current step function
        "${ROUTES[$STEP]}"
    done
}

# Run main function
main
相关推荐
艾醒(AiXing-w)1 小时前
Linux系统管理(二十)——Linux root磁盘不足?一站式应急清理方案(亲测可用)
linux·运维·服务器
小义_2 小时前
【Kubernetes】(五) pod2
linux·云原生·容器·kubernetes
哇哦9823 小时前
渗透安全(渗透防御)②
linux·安全·渗透防御
chao_6666664 小时前
AI coding 代码开发规范
linux·运维·服务器
xiaobangsky4 小时前
Linux SMB/CIFS 网络挂载配置指南
linux·运维·网络
wang09074 小时前
Linux性能优化之内存管理基础知识
java·linux·性能优化
杰 .4 小时前
闲暇时刻对LinuxOS的部分理解(一)
linux·服务器
摩斯电码4 小时前
深入 perf 第二版(二):用原始事件编号解锁 CPU 的隐藏指标
linux·性能优化
代码中介商4 小时前
Linux 基础命令完全指南:从文件操作到进程管理
linux·运维·服务器