如何将 GUI 添加到 Linux Shell 脚本

在 Bash 脚本中加入图形用户界面元素,例如窗口、滑块、单选按钮和进度条,是完全可行的。本文将向您展示如何利用 zenity 工具包,让您的 Bash 脚本焕然一新,展现现代化的风采。

Bash 脚本 是一种强大的编程语言,因其内置于 Bash shell 中而易于使用。它是一种解释型语言,这意味着您无需编译即可直接运行脚本。只需编辑脚本文件并使其可执行,即可立即执行,这使得开发和调试过程高效便捷。

尽管 Bash 脚本功能强大,但用户通常会提出两个主要抱怨。首先是执行速度。由于 Bash shell 需要解释脚本中的命令,因此其执行速度不如编译后的代码。但这就像抱怨拖拉机不如汽车快一样,它们各自适用于不同的场景。

然而,在实际应用中,您通常可以快速编写一个 Bash 脚本来完成任务,这比使用 C 语言等编译型语言 要快得多。

第二个抱怨是 Bash 脚本的用户界面,通常只是一个终端窗口。当然,在某些情况下,界面并不重要,例如,如果脚本只由作者本人使用,或者用于后台和批处理任务时,用户交互的需求较少,这时界面可能就无关紧要了。

但有时,您确实需要更直观和现代的用户界面。大多数用户都熟悉 图形用户界面。为了提供更流畅的用户体验,您需要在脚本中创建和使用 GUI 元素。

zenity 应用

zenity 允许您在 Bash 脚本中嵌入各种图形界面元素。它是一个功能强大的工具包,可以使您的脚本拥有现代且熟悉的外观。

zenity 预装在 Ubuntu、Fedora 和 Manjaro 等发行版中,它是 GNOME 的一部分。如果您使用 KDE,您可能需要考虑使用 kdialog,尽管 zenity 也可以在任何桌面环境中使用。

本文的示例将展示如何从命令行创建不同的对话框窗口,如何在变量中捕获它们的返回值和用户选择,以及如何在脚本中使用这些对话框窗口。

我们将演示一个使用三种不同类型对话窗口的小应用。

日历对话框窗口

日历对话窗口允许用户选择日期。使用 zenity 创建一个日历窗口非常简单,只需执行以下命令:

zenity --calendar

这将弹出一个日历对话窗口,它具备标准日期选择器的所有功能。您可以切换月份和年份,然后单击某一天来选择该日期。默认情况下,窗口打开时,今天的日期会被高亮显示。

单击“确定”将关闭对话窗口并选择高亮显示的日期。双击日期也可以达到相同的效果。

如果您不想选择日期,可以单击“取消”,按下键盘上的“Esc”键,或直接关闭对话窗口。

在上例中,选择了 2019 年 8 月 19 日。如果用户单击“确定”,日历将关闭,所选日期将被打印到终端窗口。

您可以忽略这一行消息:“GTKDialog mapped without a transient parent. This is discouraged.”

GTK 代表 GIMP 工具包,它是用于开发 GNOME 界面的工具包。最初由 GNU Image Manipulation Program ( GIMP ) 开发。GNU 代表 GNU’s Not Unix

GTK 引擎警告 zenity 的作者,他们以非标准的方式使用了 GTK 组件。

捕获日期值

将日期打印到终端可能并无实际意义。如果我们要从脚本中调用此日历,我们需要捕获所选的日期值,以便在脚本中加以利用。我们还需要对日历进行一些自定义。

我们将在日历中使用以下选项,它们都必须以双破折号“–”开头:

  • --text:指定要在日历中显示的文本字符串,取代默认的“从下面选择一个日期”。
  • --title:设置日历对话窗口的标题。
  • --day:设置日历打开时选中的日期。
  • --month:设置日历打开时选中的月份。
  • --year:设置日历打开时选中的年份。

我们使用名为 ChosenDate 的变量来捕获从日历返回的日期。然后使用 echo $ChosenDate 将日期打印到终端窗口。

虽然前一个示例也得到了相同的结果,但在这里,我们将选定的日期存储在一个变量中。在前面的示例中,日期被打印后就丢失了。

ChosenDate=$(zenity -- calendar --text "Choose a date" --title "How-To Geek Rota" --day 1 -- month 9 --year 2019); echo $ChosenDate

现在,日历将显示我们的提示信息和窗口标题。日期被设置为我们选择的开始日期,而不是今天的日期。

我们还可以自定义返回的日期字符串格式。--date-format 选项后必须跟一个格式说明符。这是一个标记字符串,用于定义要包含在输出中的数据和格式。这些标记与 strftime() C 语言函数 中使用的标记相同,并且有很多可选项。

我们使用的标记有:

  • %A:星期几的全名。
  • %d:以数字表示的月份中的日期。
  • %m:以数字表示的月份。
  • %y:两位数的年份(不包含世纪)。
ChosenDate=$(zenity -- calendar --text "Choose a date" --title "How-To Geek Rota" --date-format="%A %d/%m/%y" --day 1 -- month 9 --year 2019); echo $ChosenDate

当用户选择日期后:

将按照我们指定的格式返回日期。它将显示星期几的名称,然后是欧洲格式的日期:日、月、年。

文件选择对话框窗口:选择文件

文件选择对话框窗口功能相当复杂。用户可以浏览文件系统,选择一个或多个文件,然后单击“确定”以选择它们,或者完全取消选择。

zenity 提供了所有这些功能,并且易于使用,就像日历对话窗口一样。

我们将使用的新选项包括:

  • --file-selection:告诉 zenity 我们要使用文件选择对话窗口。
  • --multiple:允许用户选择多个文件。
  • --file-filter:指定文件对话窗口应显示的文件类型。
zenity --file-selection --tile "How-To Geek" --multiple --file-filter="*.mm *.png *.page *.sh *.txt"

文件选择对话框窗口的功能与其他文件选择窗口类似。

用户可以浏览文件系统并选择所需的文件。

我们已经浏览到一个新目录,并选择了一个名为“button_hybrid.png”的文件。

当用户单击“确定”时,文件选择对话窗口将关闭,文件名和路径将打印到终端窗口。

如果您需要在后续处理中使用文件名,您可以像处理日历中的日期那样,将其捕获到一个变量中。

文件选择对话框窗口:保存文件

如果我们添加一个选项,可以将文件选择对话窗口转换为文件保存对话窗口。这个选项是 --save。我们还将使用 --confirm-overwrite 选项,该选项将提示用户确认是否要覆盖现有文件。

Response=$(zenity --file-selection --save --confirm-overwrite); echo $Response

此时将出现文件保存对话框窗口。请注意,对话框中包含一个允许用户键入文件名的文本字段。

用户可以浏览到文件系统中的目标位置,输入文件名,或者单击现有文件以覆盖它。

在上例中,用户选择了一个现有文件。

当他单击“确定”时,会出现一个确认对话框,要求他确认是否要替换现有文件。请注意,文件名出现在警告对话框中,这种对细节的关注体现了 zenity 的专业性。

如果我们没有使用 --confirm-overwrite 选项,该文件将被静默覆盖。

文件名被存储在 Response 变量中,然后该变量被打印到终端窗口。

通知对话框窗口

使用 zenity,在脚本中加入简洁的通知对话框窗口非常简单。您可以轻松调用各种常用的对话窗口,为用户提供信息、警告、错误消息或提出问题。

要创建一个错误消息对话框窗口,请使用以下命令:

zenity --error --width 300 --text "Permission denied. Cannot write to the file."

我们使用的新选项包括:

  • --error:告诉 zenity 我们要使用错误对话框窗口。
  • --width:设置窗口的初始宽度。

错误对话框窗口将以指定的宽度显示,并使用标准的 GTK 错误图标。

要创建信息对话窗口,请使用以下命令:

zenity --info --width 300 --text "Update complete. Click OK to continue."

我们使用的新选项是 --info,它告诉 zenity 创建一个信息对话窗口。

要创建问题对话窗口,请使用以下命令:

zenity --question --width 300 --text "Are you happy to proceed?"; echo $?

我们使用的新选项是 --question,它告诉 zenity 创建一个问题对话窗口。

$? 是一个 特殊参数。它保存最近执行的前台管道的返回值,通常是最近关闭的进程返回的值。值为零表示“OK”,一个或多个值表示“取消”。

这是一种通用技术,适用于任何 zenity 对话窗口。通过在脚本中检查此值,您可以判断对话窗口返回的数据是否应该被处理或忽略。

我们单击“是”,因此返回代码为零,表示“OK”。

要创建警告对话框窗口,请使用以下命令:

zenity --warning --title "Low Hard Drive Space" --width 300 --text "There may not be enough hard drive space to save the backup."

我们使用的新选项是 --warning,它告诉 zenity 创建一个警告对话框窗口。

此时将出现警告对话框窗口,它只有一个按钮。

进度对话框窗口

您可以使用 zenity 进度对话框窗口来显示进度条,指示脚本的执行进度。

进度条会根据脚本中的输入值进行推进。为了演示原理,请使用以下命令:

(for i in $(seq 0 10 100); do echo $i; sleep 1; done)

此命令的含义如下:

  • seq 命令以 10 为步长遍历 0 到 100 的序列。
  • 在每一步,值都会存储在变量 i 中,并被打印到终端窗口。
  • 由于 sleep 1 命令,命令会暂停一秒钟。

我们可以将它与 zenity 进度对话框窗口一起使用来演示进度条。 请注意,我们将上一个命令的输出通过管道传递给 zenity

(for i in $(seq 0 10 100); do echo $i; sleep 1; done) | zenity --progress --title "How-To Geek" -- auto-close

我们使用的新选项包括:

  • --progress:告诉 zenity 我们要使用进度对话框窗口。
  • --auto-close:当进度条达到 100% 时关闭对话框。

进度对话框窗口会出现,进度条会逐渐前进到 100%,并在每一步之间暂停一秒钟。

我们可以利用将值通过管道传递给 zenity 的概念,在脚本中包含进度对话框窗口。

在编辑器中输入以下文本并将其保存为“progress.sh”:

#!/bin/bash

function work-list () {
  echo "# First work item" 
  echo "25"
  sleep 1
  echo "# Second work item" 
  echo "50"
  sleep 1
  echo "# Third work item" 
  echo "75"
  sleep 1
  echo "# Last work item" 
  echo "100"
  sleep 1
}

work-list | zenity --progress --title "How-To Geek" --auto-close
exit 0

此脚本的详细说明如下:

  • 该脚本定义了一个名为 work-list 的函数,您可以在其中添加执行实际工作的命令和指令。您可以将每个 sleep 1 命令替换为实际的命令。
  • zenity 接受以 # 开头的回显行,并将其显示在进度对话框窗口中。您可以更改这些行的文本,以便向用户传递有用的信息。
  • 包含数字的回显行(例如 echo "25")也会被 zenity 接受,并用于设置进度条的值。
  • work-list 函数被调用,并通过管道传递给 zenity

使用以下命令使脚本可执行:

chmod +x progress.sh

使用以下命令运行脚本:

./progress.sh

脚本开始运行,文本消息会随着脚本的每个阶段而发生变化。进度条会逐步向 100% 移动。

滑块对话框窗口

滑块对话框窗口允许用户移动滑块来选择数值。这意味着用户不能输入过高或过低的值。

我们使用的新选项包括:

  • --scale:告诉 zenity 我们要使用滑块对话框窗口。
  • --min-value:设置刻度的最小值。
  • --max-value:设置刻度的最大值。
  • --step:设置使用箭头键时滑块移动的量。如果用户使用鼠标,这将不会影响滑块的移动。
  • --value:设置滑块的初始值和位置。

以下是我们使用的命令:

Response=$(zenity --scale --title "How-To Geek" --text "Select magnification." --min-value=0 --max-value=30 --step=3 --value15); echo $Response

滑块对话窗口出现,滑块被设置为 15。

用户可以移动滑块来选择新值。

当用户单击“确定”时,该值将被传递到 Response 变量中,并被打印到终端窗口。

条目对话窗口

条目对话窗口允许用户输入文本。

我们使用的新选项包括:

  • --entry:告诉 zenity 我们要使用条目对话窗口。
  • --entry-text:如果您想在文本输入字段中预填一个建议值,可以使用此选项。我们在此处使用 "" 来强制一个空字段。这不是严格必需的,但我们在此记录了此选项。

完整的命令如下所示:

Response=$(zenity --entry --text "Enter your search term" --title "Howe-To Geek" --entry-text=""); echo $Response

将出现一个简单的对话窗口,其中包含一个文本输入字段。

用户可以键入和编辑文本。

当他单击“确定”时,他输入的值将赋值给 Response 变量。我们使用 echo 在终端窗口中打印该变量的值。

将它们组合在一起

现在,让我们将这些技术整合在一起,创建一个功能性的脚本。该脚本将执行硬件信息扫描,并在滚动文本窗口中将结果呈现给用户。用户可以选择执行长扫描或短扫描。

对于这个脚本,我们将使用三种类型的对话窗口,其中两种对我们来说是新的:

  • 第一个是列表对话窗口,它允许用户进行选择。
  • 第二个是进度对话窗口,它让用户知道正在发生的事情,并表明他应该等待。
  • 第三个是文本信息窗口,它向用户显示结果。

在编辑器中输入以下文本并将其保存为“hardware-info.sh”:

#!/bin/bash
# Display hardware listing for this computer
TempFile=$(mktemp)
ListType=`zenity --width=400 --height=275 --list --radiolist 
     --title 'Hardware Scan' 
     --text 'Select the scan type:' 
     --column 'Select' 
     --column 'Scan Type' TRUE "Short" FALSE "Long"`
if [[ $? -eq 1 ]]; then
  # they pressed Cancel or closed the dialog window 
  zenity --error --title="Scan Declined" --width=200 
       --text="Hardware scan skipped"
  exit 1
elif [ $ListType == "Short" ]; then
  # they selected the short radio button 
  Flag="--short"
else
  # they selected the long radio button 
  Flag="" 
fi
# search for hardware info with the appropriate value in $Flag
hwinfo $Flag | tee >(zenity --width=200 --height=100 
     --title="Collating Information" --progress 
     --pulsate --text="Checking hardware..." 
     --auto-kill --auto-close) >${TempFile}
# Display the hardware info in a scrolling window
zenity --width=800 --height=600 
     --title "Hardware Details" 
     --text-info --filename="${TempFile}"
exit 0

使用以下命令使脚本可执行:

chmod +x hardware-info.sh

此脚本首先创建一个临时文件,并将文件名保存在 TempFile 变量中:

TempFile=$(mktemp)

脚本使用