使用 dialog 和 jq 在 Linux 上編寫高效終端 TUI
為何選擇文字用戶界面(TUI)?
許多人每日都在使用終端,因此,文字用戶界面Text User Interface(TUI)逐漸顯示出其價(jià)值。它能減少用戶輸入命令時(shí)的誤差,讓終端操作更高效,提高生產(chǎn)力。
以我的個(gè)人使用情況為例:我每日會(huì)通過(guò)家用電腦遠(yuǎn)程連接到我使用 Linux 系統(tǒng)的實(shí)體 PC。所有的遠(yuǎn)程網(wǎng)絡(luò)連接都通過(guò)私有 VPN 加密保護(hù)。然而,當(dāng)我需要頻繁重復(fù)輸入命令進(jìn)行連接時(shí),這種經(jīng)歷實(shí)在令人煩躁。
于是,我創(chuàng)建了下面這個(gè) Bash 函數(shù),從而有所改進(jìn):
export REMOTE_RDP_USER="myremoteuser"
function remote_machine() {
/usr/bin/xfreerdp /cert-ignore /sound:sys:alsa /f /u:$REMOTE_RDP_USER /v:$1 /p:$2
}
但后來(lái),我發(fā)現(xiàn)自己還是頻繁地執(zhí)行下面這條命令(在一行中):
remote_pass=(/bin/cat/.mypassfile) remote_machine $remote_machine $remote_pass
這太煩了。更糟糕的是,我的密碼被明文存儲(chǔ)在我的電腦上(我雖然使用了加密驅(qū)動(dòng)器,但這點(diǎn)依然令人不安)。
因此,我決定投入一些時(shí)間,編寫一個(gè)實(shí)用的腳本,從而更好地滿足我的基本需求。
我需要哪些信息才能連接到遠(yuǎn)程桌面?
實(shí)際上,要連接到遠(yuǎn)程桌面,你只需提供少量信息。這些信息需要進(jìn)行結(jié)構(gòu)化處理,所以一個(gè)簡(jiǎn)單的 JSON 文件就能夠滿足要求:
{"machines": [
{
"name": "machine1.domain.com",
"description": "Personal-PC"
},
{
"name": "machine2.domain.com",
"description": "Virtual-Machine"
}
],
"remote_user": "MYUSER@DOMAIN",
"title" : "MY COMPANY RDP connection"
}
盡管在各種配置文件格式中,JSON 并非最佳選擇(例如,它不支持注解),但是 Linux 提供了許多工具通過(guò)命令行方式解析 JSON 內(nèi)容。其中,特別值得一提的工具就是 jq。下面我要向你展示如何利用它來(lái)提取機(jī)器列表:
/usr/bin/jq --compact-output --raw-output '.machines[]| .name' \
$HOME/.config/scripts/kodegeek_rdp.json) \
"machine1.domain.com" "machine2.domain.com"
jq
的文檔可以在 這里 找到。另外,你也可以直接將你的 JSON 文件復(fù)制粘貼到 jq play,試用你的表達(dá)式,然后在你的腳本中使用這些表達(dá)式。
既然已經(jīng)準(zhǔn)備好了連接遠(yuǎn)程計(jì)算機(jī)所需的所有信息,那現(xiàn)在就讓我們來(lái)創(chuàng)建一個(gè)美觀實(shí)用的 TUI 吧。
Dialog 的幫助
Dialog 是那些你可能希望早些認(rèn)識(shí)的、被低評(píng)估的 Linux 工具之一。你可以利用它構(gòu)建出一個(gè)井然有序、簡(jiǎn)介易懂,并且完美適用于你終端的用戶界面。
比如,我可以創(chuàng)建一個(gè)包含我喜歡的編程語(yǔ)言的簡(jiǎn)單的復(fù)選框列表,且默認(rèn)選擇 Python:
dialog --clear --checklist "Favorite programming languages:" 10 30 7\
1 Python on 2 Java off 3 Bash off 4 Perl off 5 Ruby off
我們通過(guò)這條命令向 dialog
下達(dá)了幾個(gè)指令:
- 清除屏幕(所有選項(xiàng)都以
--
開(kāi)頭) - 創(chuàng)建一個(gè)帶有標(biāo)題的復(fù)選框(第一個(gè)位置參數(shù))
- 決定窗口尺寸(高度、寬度和列表高度,共 3 個(gè)參數(shù))
- 列表中的每條選項(xiàng)都由一個(gè)標(biāo)簽和一個(gè)值組成。
驚人的是,僅僅一行代碼,就帶來(lái)了簡(jiǎn)潔直觀和視覺(jué)友好的選擇列表。
關(guān)于 dialog
的詳細(xì)文檔,你可以在 這里 閱讀。
整合所有元素:使用 Dialog 和 JQ 編寫 TUI
我編寫了一個(gè) TUI,它使用 jq
從我的 JSON 文件中提取配置詳細(xì)信息,并且使用 dialog
來(lái)組織流程。每次運(yùn)行,我都會(huì)要求輸入密碼,并將其保存在一個(gè)臨時(shí)文件中,腳本使用后便會(huì)刪除這個(gè)臨時(shí)文件。
這個(gè)腳本非?;A(chǔ),但它更安全,也使我能夠?qū)W⒂诟匾娜蝿?wù) ??
那么 腳本 看起來(lái)是怎樣的呢?下面是代碼:
#!/bin/bash
# Author Jose Vicente Nunez
# Do not use this script on a public computer. It is not secure...
# https://invisible-island.net/dialog/
# Below some constants to make it easier to handle Dialog
# return codes
: ${DIALOG_OK=0}
: ${DIALOG_CANCEL=1}
: ${DIALOG_HELP=2}
: ${DIALOG_EXTRA=3}
: ${DIALOG_ITEM_HELP=4}
: ${DIALOG_ESC=255}
# Temporary file to store sensitive data. Use a 'trap' to remove
# at the end of the script or if it gets interrupted
declare tmp_file=$(/usr/bin/mktemp 2>/dev/null) || declare tmp_file=/tmp/test$$
trap "/bin/rm -f $tmp_file" QUIT EXIT INT
/bin/chmod go-wrx ${tmp_file} > /dev/null 2>&1
:<<DOC
Extract details like title, remote user and machines using jq from the JSON file
Use a subshell for the machine list
DOC
declare TITLE=$(/usr/bin/jq --compact-output --raw-output '.title' $HOME/.config/scripts/kodegeek_rdp.json)|| exit 100
declare REMOTE_USER=$(/usr/bin/jq --compact-output --raw-output '.remote_user' $HOME/.config/scripts/kodegeek_rdp.json)|| exit 100
declare MACHINES=$(
declare tmp_file2=$(/usr/bin/mktemp 2>/dev/null) || declare tmp_file2=/tmp/test$$
# trap "/bin/rm -f $tmp_file2" 0 1 2 5 15 EXIT INT
declare -a MACHINE_INFO=$(/usr/bin/jq --compact-output --raw-output '.machines[]| join(",")' $HOME/.config/scripts/kodegeek_rdp.json > $tmp_file2)
declare -i i=0
while read line; do
declare machine=$(echo $line| /usr/bin/cut -d',' -f1)
declare desc=$(echo $line| /usr/bin/cut -d',' -f2)
declare toggle=off
if [ $i -eq 0 ]; then
toggle=on
((i=i+1))
fi
echo $machine $desc $toggle
done < $tmp_file2
/bin/cp /dev/null $tmp_file2
) || exit 100
# Create a dialog with a radio list and let the user select the
# remote machine
/usr/bin/dialog \
--clear \
--title "$TITLE" \
--radiolist "Which machine do you want to use?" 20 61 2 \
$MACHINES 2> ${tmp_file}
return_value=$?
# Handle the return codes from the machine selection in the
# previous step
export remote_machine=""
case $return_value in
$DIALOG_OK)
export remote_machine=$(/bin/cat ${tmp_file})
;;
$DIALOG_CANCEL)
echo "Cancel pressed.";;
$DIALOG_HELP)
echo "Help pressed.";;
$DIALOG_EXTRA)
echo "Extra button pressed.";;
$DIALOG_ITEM_HELP)
echo "Item-help button pressed.";;
$DIALOG_ESC)
if test -s $tmp_file ; then
/bin/rm -f $tmp_file
else
echo "ESC pressed."
fi
;;
esac
# No machine selected? No service ...
if [ -z "${remote_machine}" ]; then
/usr/bin/dialog \
--clear \
--title "Error, no machine selected?" --clear "$@" \
--msgbox "No machine was selected!. Will exit now..." 15 30
exit 100
fi
# Send 4 packets to the remote machine. I assume your network
# administration allows ICMP packets
# If there is an error show message box
/bin/ping -c 4 ${remote_machine} >/dev/null 2>&1
if [ $? -ne 0 ]; then
/usr/bin/dialog \
--clear \
--title "VPN issues or machine is off?" --clear "$@" \
--msgbox "Could not ping ${remote_machine}. Time to troubleshoot..." 15 50
exit 100
fi
# Remote machine is visible, ask for credentials and handle user
# choices (like password with a password box)
/bin/rm -f ${tmp_file}
/usr/bin/dialog \
--title "$TITLE" \
--clear \
--insecure \
--passwordbox "Please enter your Windows password for ${remote_machine}\n" 16 51 2> $tmp_file
return_value=$?
case $return_value in
$DIALOG_OK)
# We have all the information, try to connect using RDP protocol
/usr/bin/mkdir -p -v $HOME/logs
/usr/bin/xfreerdp /cert-ignore /sound:sys:alsa /f /u:$REMOTE_USER /v:${remote_machine} /p:$(/bin/cat ${tmp_file})| \
/usr/bin/tee $HOME/logs/$(/usr/bin/basename $0)-$remote_machine.log
;;
$DIALOG_CANCEL)
echo "Cancel pressed.";;
$DIALOG_HELP)
echo "Help pressed.";;
$DIALOG_EXTRA)
echo "Extra button pressed.";;
$DIALOG_ITEM_HELP)
echo "Item-help button pressed.";;
$DIALOG_ESC)
if test -s $tmp_file ; then
/bin/rm -f $tmp_file
else
echo "ESC pressed."
fi
;;
esac
你從代碼中可以看出,dialog
預(yù)期的是位置參數(shù),并且允許你在變量中捕獲用戶的回應(yīng)。這實(shí)際上使其成為編寫文本用戶界面的 Bash 擴(kuò)展。
上述的小例子只涵蓋了一些部件的使用,其實(shí)還有更多的文檔在 官方 dialog 網(wǎng)站上。
Dialog 和 JQ 是最好的選擇嗎?
實(shí)現(xiàn)這個(gè)功能可以有很多方法(如 Textual,Gnome 的 Zenity,Python 的 TKinker等)。我只是想向你展示一種高效的方式——僅用 100 行代碼就完成了這項(xiàng)任務(wù)。
確實(shí),它并不完美。更具體地講,它與 Bash 的深度集成使得代碼有些冗長(zhǎng),但仍然保持了易于調(diào)試和維護(hù)的特性。相比于反復(fù)復(fù)制粘貼長(zhǎng)長(zhǎng)的命令,這無(wú)疑是一個(gè)更好的選擇。
最后,如果你喜歡在 Bash 中使用 jq
處理 JSON,那么你會(huì)對(duì)這個(gè) jq 配方的精彩集合 感興趣的。