基于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
相关推荐
A小辣椒17 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒21 小时前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩3 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言