【动机】
上一篇改进的菜单(一), 主要存在的问题是, 如果菜单项和(或)值包含单引号, 得到的结果可能不是预期. 本篇尝试解决.
【意图】
上述问题的根源在于, 菜单项与对应命令组成的描述字符串, 本身已经是单引号嵌套于双引号的形式, 所以在单引号内部, 继续嵌套单引号, 难度加大. 所以, 尝试摒弃描述字符串, 改为传入两个索引数组, 一个用于保存菜单项文本, 一个用于保存菜单项对应的命令(函数). 之所以采用两个索引数组, 而不是关联数组, 在于后者传入函数后, 元素顺序变为不可预知.
【预备函数】
在 data.sh 中, 新建 check_var_name, 用于检查变量名称是否合法.
:<<COMMENT
$1: 检查可能会作为变量名的字符串是否合法
判断一个字符串是否可以作为有效的变量名
返回值:
1) y 表示可以作为变量名;
2) 空 表示不可以
用法: check_var_name <v_name>
COMMENT
check_var_name(){
# [[ "$1" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] && echo y
[[ "$1" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] && return 0 || return 1
}
【实现】
在 menu.sh 中新建函数 adult_menu 和 adult_assign, 表示相对于 guy_* 有所改进.
:<<COMMENT
根据提供的选项列表, 生成选择菜单
guy_menu 的升级版本, 菜单项设置上稍稍复杂, 主要是为了应对命令行参数中可能包含的引号. 所以比前者更健壮
参数
--title: 标题, 默认值 Available Operation Choices
--title-align: 标题的对齐方式, 可用值 left right center, 默认值 center
--title-clrs: 标题的颜色字符串, 默认 "33;1"
--item-align: 菜单项的对齐方式, 可用值 left right center, 默认值 left
--item-clrs: 菜单项的颜色字符串, 默认为 "36;1"
--prompt: 选择的提示用语, 默认是 welcome, which operation do you want
--prompt-align: 选择的提示用语的对齐方式
--prompt-clrs: 选择的提示用语采用的颜色字符串, 默认为 "34;1"
--pads: 标题/菜单项/选择提示用语对齐方式为 left 或 right 时, 添加的先导(对于left)或后缀(对于right)空格数量. 默认为 4
--menu_item_texts 菜单项文本数组名称
--menu_item_cmds 菜单项对应的命令(函数)数组名称
注意: 菜单项及对应命令分别位于不同的数组, 而不是同时位于一个关联数组, 是为了枚举时按索引(数字)排序, 以便所得与调用时所见相同.
使用:
1. 所有配置全部定制的使用形式:
local -r item_texts=("执行 funA, 无参数" "执行 funB, 带参数 hello" "示范最简使用方式"
"示范动态生成的菜单项" "...返回上一步" "...退出程序")
local -r item_cmds=("funA; choice" "funB \" ' hell' o; choice" mini_choice
dynamic_choic start "")
adult_menu --title "please select a choice" \
--menu-item-texts item_texts --menu-item-cmds item_cmds \
--title-align left --item-align center --prompt-align right \
--title-clrs "35;42;4" -item-clrs "33;46;2" --prompt-clrs "31;44;5" \
--prompt "请选择操作"
2. 全部采用默认值, 除了菜单项:
adult_menu --menu-item-texts item_texts --menu-item-cmds item_cmds
COMMENT
adult_menu(){
local -r reg_clrs="^[;0-9]+$"
local -Ar clrs_defaults=([title]="33;1" [item]="36;1" [prompt]="34;1")
local -Ar align_defaults=([title]=center [item]=left [prompt]=left)
eval $PASX
local menu_item_texts menu_item_cmds \
title prompt pads \
title_align item_align prompt_align \
title_clrs item_clrs prompt_clrs
# 所有选项参数
params "title 'Available Operation Choices'" \
"prompt 'welcome, which operation do you want'" \
pads menu_item_texts menu_item_cmds \
title-align item-align prompt-align \
title-clrs item-clrs prompt-clrs
# 修正
pads=$(ensure_integer 4 "$pads")
local key v_n
for key in ${!align_defaults[@]}; do
v_n=${key}_align
# eval 后的字串, 标志函数调用的 $ 必须转义
eval "$v_n=\$(available_range \"${!v_n}\" ${align_defaults[$key]} left center right)"
done
for key in ${!clrs_defaults[@]}; do
v_n=${key}_clrs
[[ ! "${!v_n}" =~ $reg_clrs ]] && eval "$v_n=\"${clrs_defaults[$key]}\""
done
# 打印标题
aopx $pads "echox -e" $title_align "\e[${title_clrs}m$title\e[0m"
# 打印菜单项
local pair line idx
local -n __ref_texts__=$menu_item_texts
local -n __ref_cmds__=$menu_item_cmds
local count=${#__ref_texts__[@]}
log_info "----${__ref_texts__[@]}----------"
#describe_array __ref_cmds__
for((idx=0;idx<count;idx++)); do
printf -v line "%2d) %s" $[idx+1] "${__ref_texts__[idx]}"
aopx $pads "echox -e" $item_align "\e[${item_clrs}m$line\e[0m"
done
#describe_array __ref_cmds__
# 选择, 使用序号
local _choice_=-1 hint
until [[ "$_choice_" =~ ^[0-9]+$ && $_choice_ -ge 1 && $_choice_ -le $count ]] ; do
printf -v hint "%s(input index: 1-%d, then press enter): " "$prompt" $count
aopx $pads "echox -ne" $prompt_align "\e[${prompt_clrs}m$hint\e[0m"
read _choice_
done
# 对应的命令行(函数)
local cmdline=${__ref_cmds__[_choice_-1]}
[ "$__DEBUG__" ] && log_info "prepare to eval original: <$cmdline>"
# eval "$cmdline" # error when including double quote
# eval "${cmdline//\"/\\\"}" # 将内嵌的所有双引号转义 ok
evalx $cmdline
}
:<<COMMENT
从指定的列表数据中, 根据用户的选择, 将指定项数据赋值给指定名称的变量
guy_assign 的升级版本, 菜单项设置上稍稍复杂, 主要是为了应对提供的目标值中可能包含的引号. 所以比前者更健壮
参数
--exe-flag: 执行标志字符串, 用来标识某个菜单项对应的值字符串, 是为执行命令
而提供的命令行(函数及参数)字符串, 而非为了赋值. 这主要是为了
让客户端除了使用菜单项对应的值给变量赋值外, 还可以提供额外的
等同于 adult_menu 的实际命令, 例如 "回到上一步", "退出程序" 之类,
当然也可以是其他命令. 默认为双花括号 {{}}
--v-name: 选择的结果, 将赋值给该名称指向的变量值
以下选项参数, 意义与 adult_menu 一致, 意味着它们将被原封不动传入其中
--title: 选择菜单的标题, 默认为 "choose a value from list"
--title-align: 标题的对齐方式, 可用值 left right center, 默认值 center
--title-clrs: 标题的颜色字符串, 默认 "33;1"
--item-align: 菜单项的对齐方式, 可用值 left right center, 默认值 left
--item-clrs: 菜单项的颜色字符串, 默认为 "36;1"
--prompt: 选择的提示用语, 默认是 enter your choice
--prompt-align: 选择的提示用语的对齐方式
--prompt-clrs: 选择的提示用语采用的颜色字符串, 默认为 "34;1"
--pads: 标题/菜单项/选择提示用语对齐方式为 left 或 right 时, 添加的先导(对于left)或后缀(对于right)空格数量. 默认为 4
--menu_item_texts 菜单项文本数组名称
--menu_item_values 菜单项对应的值数组. 根据元素是否以 exe-flag 开头, 判断是执行命令, 还是赋值
用法:
local choice # --item-clrs "31;46;4"
local mygod="yes, I'm an engineer." # ' 无效果
# 注意有一个为空的菜单项
local item_texts=(
''
"2010 - 04 - 12"
"2012- 05-09, 有意试试空格作为 value"
"使用 2009 年 05 月 30 日的记录"
"use 2018/23/08 \' ' record' "
"您好, 您还可以直接返回上一步"
"当然, 您也可以退出程序")
local item_values=(
''
''
' '
'2009 - 05 -30'
"2 0 $mygod \" you are a teacher. \' ' ''' 18-08-23"
" +=+ dynamic_choic --no-such-parameter xyz "
" exit ")
adult_assign --exe-flag '+=+' --v-name choice \
--title "当前存在的可以删除的记录列表, 操作完成自动返回上一步" --title-align right --title-clrs "35;42;2" \
--item-align center \
--prompt "输入要删除 的记录 的编号 " --prompt-align center --prompt-clrs "33;44;1" \
--pads 8 --menu-item-texts item_texts --menu-item-values item_values
COMMENT
adult_assign(){
local v_name exe_flag menu_item_values menu_item_texts
eval $PASX
params v_name "exe_flag {{}}" menu_item_values menu_item_texts
local -n values=$menu_item_values
local -n texts=$menu_item_texts
local item_cmds=() val
local flag_len=${#exe_flag} head idx temp target
local count=${#texts[@]}
if check_var_name $v_name; then
for((idx=0;idx<count;idx++)){
val=${values[$idx]}
if [ "$val" ]; then
# 先假设它仅由空格组成, 或者确实是非空格目标值
#target="$v_name=\"$val\""
target="$v_name=\${values[$idx]}" # 必须晚绑定
# 如果是命令
if [[ ! "$val" =~ ^[[:space:]]+$ ]]; then
# 不全是空格
temp=$(trim "$val")
head=${temp:0:$flag_len}
if [ "$head" == $exe_flag ]; then
# 是命令, 不是赋值
target=${temp#*$exe_flag} # 得到即时值
fi
fi
else
#target="$v_name=\"${texts[$idx]}\""
target="$v_name=\${texts[$idx]}" # 必须晚绑定
fi
item_cmds+=("$target")
}
else
log_error "<$v_name> not available for variable"
return 123
fi
# describe_array values
# describe_array texts
# describe_array item_cmds
# 虽然直接使用 $@ 会连同以下参数会一起发送过去:
# --v-name --exe-flag --menu-item-values
# 但是, adult_menu 将忽略这三个选项参数, 所以这不是问题.
# 如果一定要将它们隔离后再发送, 尚需要做很多工作, 以至于成本大于收益, 不划算
adult_menu --menu-item-cmds item_cmds "$@"
}
【测试】
新建文件 test.sh, 写入以下代码:
# 测试 adult_menu adult_assign
funA(){
tip "funA ......"
}
funB(){
echo "funB args:<$*>"
}
# 最简使用
mini_choice(){
local -r item_texts=("执行 funA, 无参数" "执行 funB, 带参数 hello" "示范最简使用方式" "示范动态生成的菜单项" "...返回上一步" "...退出程序")
local -r item_cmds=("funA; choice" "funB \" ' hell' o; choice" mini_choice dynamic_choic start "")
adult_menu --menu-item-texts item_texts --menu-item-cmds item_cmds
}
# 模拟动态生成的菜单项
dynamic_choic(){
local item_texts=() item_cmds=()
item_texts+=("执\" ' \' \\ \\\\\\\" ' 行 funA, 无参数, 但是 \" \"文本\"包含许多无聊的引号"); item_cmds+=("funA; choice" )
item_texts+=("执行 funB, 带参数 hello"); item_cmds+=("funB hello ' \" ' you're a teacher \' \"; choice" )
item_texts+=("示范多个参数明示的使用方式"); item_cmds+=(choice)
item_texts+=("示范赋值菜单"); item_cmds+=(assign_by_menu)
item_texts+=("...返回上一步"); item_cmds+=(start)
item_texts+=("...退出程序");
adult_menu --title "动态生成的选择菜单项" --menu-item-texts item_texts --menu-item-cmds item_cmds
}
assign_by_menu(){
local choice # --item-clrs "31;46;4"
local mygod="yes, I'm an engineer."
# 注意有一个为空的菜单项
local item_texts=(
''
"2010 - 04 - 12"
"2012- 05-09, 有意试试空格作为 value"
"使用 2009 年 05 月 30 日的记录"
"use 2018/23/08 \' ' record' "
"您好, 您还可以直接返回上一步"
"当然, 您也可以退出程序")
local item_values=(
''
''
' '
'2009 - 05 -30'
"2 0 $mygod \" you are a teacher. \' ' ''' 18-08-23"
" +=+ dynamic_choic --no-such-parameter xyz "
" exit ")
adult_assign --exe-flag '+=+' --v-name choice \
--title "当前存在的可以删除的记录列表, 操作完成自动返回上一步" --title-align right --title-clrs "35;42;2" \
--item-align center \
--prompt "输入要删除 的记录 的编号 " --prompt-align center --prompt-clrs "33;44;1" \
--pads 8 --menu-item-texts item_texts --menu-item-values item_values
if [ "$choice" == exit ]; then
success "go out of selection."
exit 0
else
#echo -e "choice:<$choice>"
if [ "$choice" ]; then
tip "you will delete <$choice> record."
dynamic_choic
else
tip "you abort!"
fi
fi
}
choice(){
# 注意 funB 的参数有意带上引号
local -r item_texts=("执行 funA, 无参数" "执行 funB, 带参数 hello" "示范最简使用方式" "示范动态生成的菜单项" "...返回上一步" "...退出程序")
local -r item_cmds=("funA; choice" "funB \" ' hell' o; choice" mini_choice dynamic_choic start "")
adult_menu --title "please select a choice" \
--menu-item-texts item_texts --menu-item-cmds item_cmds \
--title-align left --item-align center --prompt-align right \
--title-clrs "35;42;4" -item-clrs "33;46;2" --prompt-clrs "31;44;5" \
--prompt "请选择操作"
}
start(){
# 模拟程序开始
#read -p "here is mock starter. any key to enter choice..."
choice
}
start
info "Here is mocking exit programe."
【效果】
可以看到, I’m an engineer 已经可以正常显示. 当然, 相对于 guy_* 函数, 代价是将菜单文本和相应命令分别保存于两个索引数组(理论上将两者保存到同一个关联数组更优雅, 可惜目前最新的 bash 版本下, 尚无法定制关联数组的顺序, 这一般来说是制作菜单不能容忍的).
谢谢观看!