【动机】
百度一下, 目前的方法大约是以下几种方式:
- select 命令结合 case:
#!/bin/bash
PS3="请选择一个选项: "
select opt in "选项1" "选项2" "选项3" "退出"; do
case $opt in
"选项1")
echo "你选择了选项1"
;;
"选项2")
echo "你选择了选项2"
;;
"选项3")
echo "你选择了选项3"
;;
"退出")
break
;;
*)
echo "无效的输入"
;;
esac
done
2. read 命令结合 case:
#!/bin/bash
while true; do
echo "请选择一个选项:"
echo "1) 选项1"
echo "2) 选项2"
echo "3) 选项3"
echo "4) 退出"
read -p "请输入选项编号: " choice
case $choice in
1) echo "你选择了选项1";;
2) echo "你选择了选项2";;
3) echo "你选择了选项3";;
4) echo "退出"; break;;
*) echo "无效的输入";;
esac
done
3. dialog 工具:
#!/bin/bash
dialog --title "菜单示例" --menu "请选择一个选项" 0 0 0 \
1 "选项1" \
2 "选项2" \
3 "选项3" \
4 "退出" 2>&1 >/dev/tty
choice=$?
case $choice in
1) echo "你选择了选项1";;
2) echo "你选择了选项2";;
3) echo "你选择了选项3";;
4) echo "退出"; exit;; # 注意这里使用了exit来结束脚本执行,而不是break,因为我们已经离开了循环结构。
esac
意味着每次生成菜单, 都需要先来这么一大段循环/case, 而且同样的菜单项, 对于 select 格式的菜单, 还需要在两个位置重复书写, 味道不好.
如何通过指定以下内容, 生成菜单:
- 标题
- 菜单项文本, 对应菜单项描述, 选择该菜单项出发的命令(函数)调用, 还可以配置其他相关数据
- 可选的回到上一步菜单项
- 可选的退出程序菜单项
从而避免随时出现的冗长的 case 语句?
【意图】
将菜单的生成和对应的命令执行封装到函数, 需要菜单时配置好函数的各个选项, 调用即可.
【实现】
先创建 constant.sh 文件, 用于声明分隔符常量:
# 多行字符串的 行 / 列 分隔符(主要用于菜单及其对应的描述和命令行的生成)
# 注意并不是任何字符均适合此用途, 例如 * $ | 等
declare -r _DEF_ROW_SEP='}}'
declare -r _DEF_COL_SEP='}'
它们将被用于后面的 kid_menu 函数
新建文件 menu.sh, 写入函数 kid_menu:
:<<COMMENT
根据提供的选项列表, 结合 back_fun, 生成选择菜单
-t: title 标题文本, 默认 Available Operation Choices
-p: prompt 选择的提示用语, 默认是 welcome, which operation do you want
-s: options 由多个选项/命令行对, 构成的字符串, 字符串格式说明见后
-c: cs (即 col seperator) 每个选项与对应选项描述及其命令行分隔符, 默认为 $_DEF_COL_SEP
-r: rs (即 row seperator) 相邻的选项/命令行对之间的分隔符, 默认为 $_DEF_ROW_SEP
-b: back_cmdl 返回上一步 选项对应的命令行. 默认空, 表示无 返回上一步 选项, 或者需要自定义
-u: unNeed_exit 标志不附加 退出程序 选项, 默认空, 表示附加, 任意非空值, 表示不附加
返回值, 无
用法: kid_menu -t <title> -p <prompt> -s <options> -c <col-sep=> -r <row-sep=> -b <back_cmdl?> -u <unNeed_exit?>
注意:
1. options 表达的选项/命令行对: 每个 pair 之间, 使用 $rs 分隔符隔开;
2. 每个 pair 内部, 依次是 item 文本, description 文本, 以及 cmdl 命令行(可包括参数),
相邻两者以 $cs 分隔符隔开
3. 附加的退出程序选项, 执行的是空操作. 而不是 exit. 以保证父函数的扫尾工作正常进行
4. $rc $cs 应避开 options 中存在的字符, 例如存在组合命令时, 应避免采用分号(;)
COMMENT
kid_menu(){
eval $PAS
local title=$(para t "Available Operation Choices");
local prompt=$(para p "welcome, which operation do you want");
local options=$(para s)
local cs=$(para c "$_DEF_COL_SEP")
local rs=$(para r "$_DEF_ROW_SEP")
local back_cmdl=$(para b)
local unNeed_exit=$(para u)
[ "$back_cmdl" ] && options+="...<<返回上一步 $cs prepare to go back, please wait a while...! $cs $back_cmdl $rs"
[ -z "$unNeed_exit" ] && options+="...<<退出程序 $cs prepare to exit, byebye! $cs : $rs"
[ "$__DEBUG__" ] && log_info "options:<$options>"
local arrItem arrDesc arrCmdl
local arr_names=(arrItem arrDesc arrCmdl)
for idx in ${!arr_names[@]}; do
local arr_name=${arr_names[idx]}
# awk 列索引从 1 开始, 而 shell 数组索引从 0 开始
readarray -t $arr_name <<< $(echo "$options" | awk -v FS="$cs" -v RS="$rs" -v col=$[idx+1] \
'{
# 忽略空行
if ($0 !~ /^\s*$/){
# 去掉首尾空格(但保留中间)
sub(/^\s+/, "", $col)
sub(/\s+$/, "", $col)
print $col
}
}')
done
# log_info "items=<${arrItem[@]}>"
# log_info "descs=<${arrDesc[@]}>"
# log_info "cmdls=<${arrCmdl[@]}>"
# 标题
aop caption center $title
# 菜单项
local count=${#arrItem[@]} msg i
for((i=0;i<count;i++));do
msg=$(printf "%d) %s" $[i+1] "${arrItem[$i]}")
aop 'menu -e' left "\t$msg"
done
# 选择, 使用序号
local choice=-1
until [[ $choice =~ ^[0-9]+$ && $choice -ge 1 && $choice -le $count ]] ; do
msg=$(printf "%s(input index: 1-$count, then press enter): " "$prompt")
aop "notice -ne" left "\t$msg"
read choice
done
# 对应的描述以及命令行(函数)
aop tip right "${arrDesc[choice-1]}"
[ "$__DEBUG__" ] && log_info "prepare to eval <${arrCmdl[choice-1]}>"
eval "${arrCmdl[choice-1]}"
}
注意, kid_menu, 用到 parameter.sh, console.sh, log.sh 中的相关函数
大致的原理是: 确定三个数组 –> 写菜单项 –> 等待选择 –> 根据选择出发相应命令(函数)执行.
【测试】
从本节起, 省略 source 其他脚本文件的代码行, 读者可自由选择逐行 source , 或者使用 find 命令批量 source的形式.
新建 test.sh, 第一个测试: 默认值, 菜单项仅执行单独命令:
way1(){
funA(){
tip "funA ......"
}
funB(){
echo "funB args:<$*>"
}
local pairs
for mod in $modules; do
fun="${mod}_menu_status"
pairs+=$($fun -e $ent -r $rs -c $cs)
done
pairs+="执行 funA, 无参数 } 准备调用 funA... } funA }}"
pairs+="执行 funB, 带参数 hello } 准备调用 funB...} funB hello }}"
echo "除了菜单项, 其余全部采用默认生成菜单如下, 同时, 每个菜单项, 执行完毕也退出程序:"
kid_menu -s "$pairs"
}
way1
unset -f way1
运行结果:

第二个测试, 所有选项全部设置, 菜单项执行组合命令:
way2(){
funA(){
tip "funA ......"
}
funB(){
echo "funB args:<$*>"
}
local pairs
for mod in $modules; do
fun="${mod}_menu_status"
pairs+=$($fun -e $ent -r $rs -c $cs)
done
pairs+="执行 funA, 无参数 @ 准备调用 funA... @ funA; aop success center \"funA is over \"; way2 %%"
pairs+="执行 funB, 带参数 hello @ 准备调用 funB...@ funB hello; aop info right \"end of funB\"; way2 %%"
kid_menu -t "参数全部配置,包括分隔符" -s "$pairs" \
-p "请选择. 请勿尝试非法输入:" \
-c '@' -r '%%' -b "way1" -u y
}
way2
unset -f way2
运行结果:

可以看到, 默认的 “退出程序” 被屏蔽, 选项 1 / 2 末尾均加入执行 way2, 而返回上一步则执行 way1, 符合一般常见用法. 当然, 生成环境中使用时, way1 中还应该配置进入 way2 的菜单项, 同时 way2 中一般也应该包含退出程序选项. 这里仅仅是示范菜单的生成, 这些工作就免了.
【赋值菜单】
上一种菜单姑且称之为, 命令菜单; 实际应用中,赋值菜单也可能会用到, 理论上讲, 可以将赋值语句写成命令的形式, 然后套用前面的 kid_menu. 但为了使用更简洁, 还有新的方式, 这就是重新封装的 kid_assign 函数:
:<<COMMENT
从指定的列表数据中, 根据用户的选择, 将指定项数据赋值给指定名称的变量
-t: caption: 选择菜单的标题, 默认为 "choose a value from list"
-p: prompt: 选择提示用语, 默认为 "enter your choice"
-m: cmdl_to_prev: 返回到上一步的命令行, 默认为空, 即无此选项
-v: v_name: 选择的结果, 将赋值给该名称指向的变量值
-d: datas: 以下面的 -s 参数指定的分隔符相隔的多个数据,
-s: datas 以一个字符串表示的多个数据使用的分隔符, 当数据本身包含空格时需要修改. 默认为单个空格 ' '.
-c: cs (即 col seperator) 每个选项与对应选项描述及其命令行分隔符, 默认为 $_DEF_COL_SEP
-r: rs (即 row seperator) 相邻的选项/命令行对之间的分隔符, 默认为 $_DEF_ROW_SEP
用法:
local v_name
assign_from_menu -t <caption?> -p <prompt?> -m <cmdl_to_prev> -v <v_name> -d <datas> -c <col-sep=> -r <row-sep=>
COMMENT
kid_assign(){
eval $PAS
local caption=$(para t "choose a value from list")
local prompt=$(para p "enter your choice")
local cmdl_to_prev=$(para m)
local v_name=$(para v)
local datas=$(para d)
local sep=$(para s ' ')
local cs=$(para c "$_DEF_COL_SEP")
local rs=$(para r "$_DEF_ROW_SEP")
local pairs item title desc cmdl;
push IFS
IFS=$sep
for item in $datas;do
title=$item
desc="$item selected"
cmdl="$v_name=\"$item\""
pairs+="$title $cs $desc $cs $cmdl $rs"
# log_info "option created:<$title $cs $desc $cs $cmdl $rs>"
done
pop IFS
[ "$cmdl_to_prev" ] && \
pairs+="...<<返回上一步 $cs 正在返回到上一步, 请稍候! $cs $cmdl_to_prev $rs"
kid_menu -t "$caption" \
-p "$prompt" \
-s "$pairs" -c "$cs" -r "$rs"
}
下面测试一下.
way3(){
aop info right "........................测试不包含空格的选项"
local datas="one two three four five"
local choice
kid_assign -d "$datas" -v choice
echo "your selection: <$choice>"
aop info right ".................测试包含空格的选项"
local datas="one first 1 |second two 2 |three | four fourth 4 |"
kid_assign -d "$datas" -v choice -s '|'
echo "your selection: <$choice>"
}
way3
测试结果如下:

【继续】
但这种菜单的生成方式还可以继续改进, 详情见下一篇改进的菜单(一).