【动机】
在函数的命名参数设计篇中, 我们看到命名参数的获取和设置上的一些限制. 在本文中, 将其一次性解决.
【意图】
手工摘取各种命名/位置参数, 采用关联数组保存. 在需要时逐个查找, 支持单字母型, 单词型命名参数, 且位置参数与命名参数互不干扰.
【预备函数】
- readarrayx: 将可能嵌套的字符串(双引号内部包含单引号, 反之亦然), 转换为数组. 位于 data.sh 中
:<<COMMENT
将可能嵌套的字符串(双引号内部包含单引号, 反之亦然), 转换为数组. 注意书写上需要符合 linux shell 规范:
1. 单引号包围的字符串:
1) 不能直接嵌套单引号, 即 ' one two ' three' 错误
2) 可以使用转义单引号, 直接双引号, 转义双引号, 但必须有先导符号 $ 例如
$' one \' two \' three " hei you " \" throw a bottle \"'
2. 双引号包围的字符串:
1) 不能直接嵌套双引号, 即 " hello " world " 错误
2) 可以嵌套直接单引号, 转义单引号, 转义双引号, 其中转义单引号可能不会用于分割, 因为转义单引号
在此被作为普通字符处理. 例如:
" hello \" world ' slilly pitty ' please\" \' this is by him \' "
' this is by him ' 会被打散
3. 可以多级嵌套, 但如果嵌套不规范, 分割结果可能不是您需要的
总结起来, 推荐写法:
1) "one 'two three' 'four five six ' "
2) 'one "two three" "four five six " '
然而, 经测试, 也可应对任何奇葩的字符串, 包括但不限于各式换行符/制表符/退格符等等.
参数:
$1: 待转换为数组的字符串
$2: 目标数组的变量名称, 需要在调用端做好声明, 否则有警告. 当变量名非法, 报错并 return 1
用法: readarrayx <string> <array_name>
注意:
1. $1 与 $2 两者任意一个为空字符串, 则操作被忽略
2. 本函数的意义在于, 常规的字符串转数组语法, 内部嵌套的字符串, 不被收纳为一个元素, 而是仅仅按照指定的
分隔符(默认为换行符或者空格)分割, 这些方法有:
1) arr=($str) # 默认按空格分割, 一个单词作为一个元素
2) arr=("$str") # 整体作为一个字符串, 不分割, 得到只包含一个元素的数组
3) readarray -t arr <<< $str # 默认按换行符, 改为按空格: -d ' '
4) readarray -t arr <<< "$str" # 同上
COMMENT
readarrayx(){
if [ "$1" -a "$2" ]; then
local rest
local -n ref_arr=$2
#eval "$arr_name=()"
ref_arr=()
rest=$1
# printf -v rest "%s" "$1" # no essential
# 将换行符转换为随机字符串以便最终还原. (sed 无法动态替换换行符).
# 两个替换可应对单引号和双引号内的各式换行符
# 为避免当随机字符串的首位为 n 时可能形成的换行符, 继而下面的处理陷入死循环
# 强制首位为 k m
local rdm_symbos1=k$(random_string 8)
local rdm_symbos2=m$(random_string 9)
rest=${rest//$'\\n'/"$rdm_symbos2"}
rest=${rest//$'\n'/"$rdm_symbos1"}
#log_info "rest:<$rest>"
local sep item
while [[ ! "$rest" =~ ^[[:space:]]*$ ]] ; do
rest=$(ltrim "$rest")
#log_info "rest:<$rest>"
sep=${rest:0:1}
# 假定当前 item 是引号包围, 当 $sep 是转义引号, 则 item 为空, 需要将其作为普通字符重新计算
# 该操作可并入 else 部分, 所以上述假定可简化分支.
item=$(string_between "$rest" $sep)
# [ ... ] && [ ... ] : 后台进程中运行, 且快而多, 可能会报错:
# if [ "$sep" == "'" -o "$sep" == '"' ] && [ "$item" ]; then
if [[ "$item" && ("$sep" == "'" || "$sep" == '"') ]]; then
# 如果是引号包围的句子型字符串, 则查询下一个对应的引号
rest=${rest/#"$sep$item$sep"/} # 双引号包围, 以应对 \k \m 之类的无聊的字符串, 下同
else
# 单词型字符串, 或者 sep 是引号却没有对应字符(说明被转义), 则重新计算 item, 并查询下一个空格
item=$(printf "%s" "$rest" | sed -r "s/^([^ ]+).*$/\1/")
rest=${rest/#"$item"/}
fi
# 当两个双引号或两个单引号相邻排列, 上面的 item 将计算为空. 于是会进入 else 分支被解释成两个引号
# 所以最后的 item 应做一个判断, 将其还原为空
# 后台进程中运行, 且快而多, 可能会报错:
# [ "$item" == '""' -o "$item" == "''" ] && item=''
[[ "$item" == '""' || "$item" == "''" ]] && item=''
# 还原换行符
item=${item//$rdm_symbos1/$'\n'}
item=${item//$rdm_symbos2/$'\\n'}
# 还原转义的引号, 对于 eval 的执行, 还会起反作用------------------
# item=${item//\"/\\\"}
# item=${item//\'/\\\'}
#item=$(emb_quote "$item")
# log_info "item is <$item>"
ref_arr+=("$item")
done
fi
}
2. echox: 智能化的 echo 函数
:<<COMMENT
更智能化的 echo 版本
一. 用于解决 echo 无法单独打印 -e -n -nn -ee -EE -neEEE -nnnnee 之类字符串(以下成为类选项字符串)的问题,
并兼顾了输出字符串的精确还原(包括其 ascii 码).
二. 确保 echo 的原生命令行选项有效, 例如 -n -ne -nE 还包括了可用但反人类的 -eE -neE -nneeEE 等
原因:
1. echo 的选项为 -e -n -E, 如果想打印此三类重复字符或者其组合,
echo 只会将其理解成选项不显示或仅仅显示为换行符因为它们被解释成选项, 而不是字符串
2. printf 的选项只有 -v, 但是, 使用 printf 但不启用格式,
字符串只要与 - 开头均报错, 除了单个 - 字符构成的字符串以外. 原因在于
printf 将它们全部理解为不存在或存在的选项, 只不过对于存在的选项 -v,
报错内容稍有不同而已
3. 如果简单采用 printf 的格式输出 printf "%s" "$str", 但 str 中包含转义字符
时, 转义字符无法得到实施.
实现原理:
1. 从参数中剥离出真实的选项, 以确保 echo 的原生命令行选项有效
如果只有一个参数, 哪怕是 -n -e -eee 之类, 我们认为它是要打印的内容,
即第一个参数要成为选项, 必须至少有第二个参数才有意义.
2. 创建先导字符串: 空格+退格.
加入先导空格是为了避免类选项字符串的输出误判, 加入退格是为了平衡添加的空格
3. 欺骗 echo, 认为上面提到的类选项字符串, 它不是选项.
通过将先导字符串添加到打印内容头部得到合成字符串 composite.
4. 生成转义字符可能已经成功转义的更新的合成字符串 composite2.
用前面得到的 echo 的原生 option, 结合 composite 得到.
5. 评估 option 是否要求抑制末尾换行.
由于函数返回 echo 得到的 composite2, 即使 option 不包含 -n,
composite2 末尾仍然没有换行符(函数返回时, 最后一个换行符自动被掐掉)
故而在 printf 最终输出前, 还需要再次检查 option 是否不包含 n, 以决定
在 printf 的格式字符串里, 末尾是否添加换行符.
6. printf "%s" ...... 格式输出.
为了最终 ascii 码的正确, 而不仅仅是打印的效果显示, 需要从第三个字符开始, 避开上面添加的
前缀字符串, 达到目的.
COMMENT
echox(){
local option
# 1. 从参数中剥离出真实的选项, 以确保 echo 的原生命令行选项有效
if [[ "$1" =~ ^-[neE]+$ && "$2" ]];then
option=$1
shift 1
fi
# 2. 创建先导字符串: 空格+退格.
local prefix=$(echo -e " \b")
# 3. 欺骗 echo, 认为类选项字符串, 它不是选项.
local composite=$prefix$@
# 4. 生成转义字符可能已经成功转义的更新的合成字符串 composite2.
local composite2=$(echo $option "$prefix$@")
# 5. 评估 option 是否要求抑制末尾换行.
local tail
[[ ! "$option" =~ n ]] && tail='\n' || :
# 6. printf "%s" ...... 格式输出
printf "%s$tail" "${composite2:2}"
}
3. describe_array: 描述数组, 内部采用 array2string 函数. 位于 debug.sh
:<<COMMENT
为指定名称的数组做描述, 即 [0]=..., [1]=... 的字符串表达
$1, 与 $2: 其中之一为数组变量名, 可以是普通数组和关联数组;
另一个为此数组提供的描述字符串, 用于先行打印, 默认为等于
数组名
注意: 通过判断变量名是否定义来决断, $1 优先假定为数组名
COMMENT
describe_array(){
local __arr__ desc
if declare -p "$1" &>/dev/null; then
__arr__=$1
desc=${2:-"array $1 data:"}
elif declare -p "$2" &>/dev/null; then
__arr__=$2
desc=${1:-"array $2 data:"}
else
error "None of <$1> and <$2> is not a defined variable"
return 0
fi
echox "$desc"
array2string $__arr__
}
4. array2string: 数组转换为字符串, 类似于其他语言的自定义的 ToString(), 位于 debug.sh.
:<<COMMENT
返回数组的字符串表达形式
$1: 数组变量名, 可以是普通数组和关联数组
COMMENT
array2string(){
local -n __ref_arr__=$1
local key
for key in "${!__ref_arr__[@]}";do
tip -e "\t[$key]=<${__ref_arr__[$key]}>"
done
#printf "\b\b%2s\n"
}
测试略.
【位置/命名参数获取模块】
位于 parameter_v2.sh.
#!/bin/bash
# 详情参见文档 doc/parameter_v2.txt
#declare -r PAR_POS_KEY=' '
#declare -r PAR_PREFIX=-
declare -r PAR_SEP_KEY=SEP
declare -r PAR_SEP_VALUE='}}}'
declare -r REG_VAR=^[a-zA-Z_][a-zA-Z0-9_]*$
declare -r REG_OPT=^-+[a-zA-Z_][-a-zA-Z0-9_]*$
# 省略全局变量, 将变量定义在调用函数中. 这样就可将不同调用函数的选项参数隔离开来
# 如果调用函数不先调用 eval $PASX, 而直接调用 para 来获取参数, 将报错
# declare -g -A __param_arr__=();
PASX="
local -A __param_arr__=();
local __sep__;
_param_init \"\$@\";
"
# 两个准备提供缓存的变量, 因 _param_cache_init 的应用失败而废弃
declare __PREV_CMDLINE
declare -A __PREV_PARA_ARR
# $1: key
# true :return 0; false: return 1
_param_contains_key(){
s_ct_w $1 ${!__param_arr__[@]}
}
# $1: option name, 带零个或多个前缀 -,
# 其余部分也可以包含 -, 但作为 key, 会事先被替换成 _
# 返回: 检查是否合乎规范, 成功返回可用作关联数组的 key,
# 否则报错, return 1
_param_option_key(){
local temp=$(ltrim $1 -)
echox ${temp//-/_}
}
# 检查字符串是否可以作为变量名, 以及是否已经有相应的非全局变量
# $1: var_name
# 不可以, 日志记录错误信息; return 1
# 可以 return 0, 但如果没有预定义, 日志记录警告信息
_param_check_var(){
local ec=0
if [[ ! "$1" =~ $REG_VAR ]]; then
# 非法 option
log_error "\"$1\" is an unavailable option"
ec=1
elif ! declare -p $1 &>/dev/null; then
# 相应的变量(用来存储返回值的数组)未定义
log_warn "$1 is not pre-defined, to receive option $1, it will be global."
#else
#log_info "$1 defined already, to receive option $1"
fi
return $ec
}
# 详情参见文档 doc/parameter_v2.txt
# $1: option name
# echox: unmarked marked assigned
_param_option_stat(){
local key=$(_param_option_key $1)
_param_key_stat $key
}
# $1: key 内部不做检查 key 的合法性, 即是否可作为变量名
_param_key_stat(){
[ "${__param_arr__[$1]}" ] && echox assigned || \
(_param_contains_key $1 && echox marked || echox unmarked)
}
# 详情参见文档 doc/parameter_v2.txt
_param_assign_sep(){
local -n ref_sep=$1
local def=$2 key
shift 2
for(( i=1;i<=$#;i++)){
if [[ "${!i}" =~ $REG_OPT ]]; then
((j=i+1))
key=$(_param_option_key ${!i})
if [[ "$key" == $PAR_SEP_KEY && ! "${!j}" =~ $REG_OPT ]]; then
ref_sep=${!j}
fi
fi
}
[ -z "$ref_sep" ] && ref_sep="$def" || :
}
# 详情参见文档 doc/parameter_v2.txt
# 注意, 缓存不支持后台进程或子进程写入. 如果用文件中转, 可能开销更大
# 已废弃
_param_cache_init(){
local dect curline=$(echox "${FUNCNAME[1]}, $@" )
log_info "prev_cmd=<$__PREV_CMDLINE>, curr_cmd=<$curline>"
if [ "$__PREV_CMDLINE" == "$curline" ]; then
dect=$(declare -p __PREV_PARA_ARR)
eval "__param_arr__=${dect#*=}"
log_info "已启用缓存: ${__PREV_PARA_ARR[@]}"
else
_param_assign_sep __sep__ $PAR_SEP_VALUE "$@"
local i arg key=' '
for((i=1;i<=$#;i++)){
arg=${!i}
if [[ "$arg" =~ $REG_OPT ]]; then
# 选项名既要以一个或多个 - 开头, 后续还要符合变量命名规范
# 对于新的 key, 应立即添加 key, 并添加空值, 而不是等到有值时
# 一次性添加, 目的是为了区分选项 unmarked 和 marked 的区别
key=$(_param_option_key $arg)
! _param_contains_key $key && __param_arr__+=([$key]=) || :
else
# 剩下的一定是位置参数, 即使只包含一个短横线 -
__param_arr__[$key]+="$arg$__sep__"
key=' '
fi
}
__PREV_CMDLINE=$curline
dect=$(declare -p __param_arr__)
eval "__PREV_PARA_ARR=${dect#*=}"
log_info "已更新缓存: ${__PREV_PARA_ARR[@]}, prev_cmd=<$__PREV_CMDLINE>"
fi
}
_param_init(){
_param_assign_sep __sep__ $PAR_SEP_VALUE "$@"
local i arg key=' '
for((i=1;i<=$#;i++)){
arg=${!i}
if [[ "$arg" =~ $REG_OPT ]]; then
# 选项名既要以一个或多个 - 开头, 后续还要符合变量命名规范
# 对于新的 key, 应立即添加 key, 并添加空值, 而不是等到有值时
# 一次性添加, 目的是为了区分选项 unmarked 和 marked 的区别
key=$(_param_option_key $arg)
! _param_contains_key $key && __param_arr__+=([$key]=) || :
else
# 剩下的一定是位置参数, 即使只包含一个短横线 -
__param_arr__[$key]+="$arg$__sep__"
key=' '
fi
}
}
# 查看当前的关联数组数据分布情况, 一般仅用于调试
param_look_up(){
# 此处不能用 describe_array, 因为其中再次调用了 eval $PASX
#describe_array "所有参数的赋值情况, 空键表示位置参数" __param_arr__
echox "所有参数的赋值情况, 空键表示位置参数"
array2string __param_arr__
}
# 详情参见文档 doc/parameter_v2.txt
# $2: marked_v
# $3: unmarked_v
param(){
local _item_ message
if declare -p __param_arr__ &>/dev/null; then
# 无参数调用, echox 由所有的位置参数连接成的字符串, 中间以换行符隔开
if [ $# -eq 0 ]; then
local _str_
push IFS
IFS=$__sep__
for _item_ in ${__param_arr__[ ]}; do
[ "$_item_" ] && _str_+="$_item_\n" || :
done
pop IFS
echox "$_str_"
elif [[ $1 =~ ^[+-]?[0-9]+$ ]]; then
# $1 为纯数字, 返回该序号的位置参数. 数字从 1 递增
# index 为 1, 刚好与 awk 的第一行对应
echox "${__param_arr__[ ]}" | awk -v RS=$__sep__ -v idx=$1 '{if (NR == idx) print }'
else
# $1 为其他字符(串), 则尝试将其看作选项参数名.
# 返回相应 key 的命名参数数组元素字符串, 并将其转换为数组
# $2: marked_v
local unmarked_v=${3:-$2}
local key=$(_param_option_key $1)
if _param_check_var $key; then
local stat=$(_param_key_stat $key)
local -n ref_arg=$key # 使用引用更安全, 简练, 优雅, 而不是 eval
case $stat in
assigned)
ref_arg=()
push IFS
IFS=$__sep__
for _item_ in ${__param_arr__[$key]}; do
[ "$_item_" ] && ref_arg+=("$_item_") || :
done
pop IFS ;;
marked)
ref_arg=$2 ;;
unmarked)
ref_arg=$unmarked_v ;;
*)
# log_err "unknown stat for option $1."
return 1 ;;
esac
fi
fi
else
message="you should call eval \$PASX first"
log_error "$message"
error $message
return 13
fi
}
# 详情参见文档 doc/parameter_v2.txt
params(){
if [ $# -eq 0 ]; then
#log_info "处理所有位置参数"
echox "$(param)"
else
local args arr names m_st s_st
for args in "${@}"; do
readarrayx "$args" arr
if [[ "${arr[0]}" =~ ^[+-]?[0-9]+$ ]]; then
# 单个位置参数
#log_info "查询第 ${arr[0]} 个位置参数, 保存到变量 <${arr[1]}> 中"
if _param_check_var ${arr[1]}; then
local -n ref_v=${arr[1]} # 配置引用
ref_v=$(param ${arr[0]} "${arr[1]}") # 通过引用赋值
fi
else
readarrayx "${arr[0]}" names
if [ ${#names[@]} -gt 1 ]; then
#log_info "查询参数对: 主参数 <${names[0]}>, 副参数 <${names[1]}> 的值"
local m_key=$(_param_option_key ${names[0]})
if _param_check_var $m_key; then
# 主参数名合格, 则采用不带默认值查询. master 引用保存最终的结果
param ${names[0]}
local -n master=$m_key
if [ -z "$master" ]; then
# 主参数查询为空, 则尝试副参数
local s_key=$(_param_option_key ${names[1]})
# 副参数只检查变量名是否规范, 不检查是否已经定义相应变量,
# 因为合格的话, 需要在此临时定义
if check_var_name $s_key; then
local $s_key
local -n slave=$s_key
param "${names[1]}"
if [ "$slave" ]; then
master=("${slave[@]}")
elif [ "${arr[1]}" -o "${arr[2]}" ]; then
# 如果都搞不定, 则看两个参数的状态.
# 只要有一个被标记, 则取 arr[1], 否则取 arr[2]
# arr[1]: marked_v arr[2]: unmarked_v
[ -z "${arr[2]}" ] && arr[2]=${arr[1]}
m_st=$(_param_key_stat $m_key)
s_st=$(_param_key_stat $s_key)
# 显式赋予空值时, stat=assigned. 在此将它与 marked 等同简化比较
# 所以, 不能使用 [ $m_st == marked -o $s_st == marked ]
[ $m_st != unmarked -o $s_st != unmarked ] && \
master=${arr[1]} || master=${arr[2]}
fi
fi
fi
fi
else
#log_info "查询单参数 ${arr[0]} 的值"
param "${arr[0]}" "${arr[1]}" "${arr[2]}"
fi
fi
done
fi
}
# 详情参见文档 doc/parameter_v2.txt
param_appear(){
local opt1=${1//-/_} opt2=${2//-/_}
[[ -v __param_arr__[$opt1] || -v __param_arr__[$opt2] ]] && return 0 || return 1
}
【测试】
_display(){
for arg in ${@}; do
declare -n v_n=$arg
param $arg
describe_array "items of $arg" v_n
done
}
_display_defaults(){
for arg in ${@}; do
declare -n v_n=$arg
param $arg ' not assigned ' ' no option '
describe_array "items of $arg" v_n
done
}
_display_single_default(){
for arg in ${@}; do
declare -n v_n=$arg
param $arg 'null-vallue'
describe_array "items of $arg" v_n
done
}
way1(){
# param 函数测试
eval $PASX
param_look_up
info "1. 无参数调用, 获取所有位置参数, 以多行字符串的形式"
tip -e $(param)
info "1.1 将它们统一放入数组, 需要时可以按索引查找, 最后一个没有, 就返回空, 并不会报错:"
local p_args
readarray -t p_args <<<$(echox -e "$(param)")
printf "[0]=<%s>,[1]=<%s>,[2]=<%s>, [3]=<%s>" "${p_args[0]}" "${p_args[1]}" "${p_args[2]}" "${p_args[3]}"
info "2. 单个数字参数调用, 获取三个明显不存在的位置参数, 返回空"
tip -e "argument [-2]:<$(param -2)>,[0]:<$(param 0)>, [1000]=<$(param 1000)>"
info "3. 单个数字参数调用, 获取合法的位置参数, 注意空格, 制表符均得以精确保持"
tip -e "argument [1]:<$(param 1)>, [2]:<$(param 2)>, [3]=:<$(param 3)>"
info "4. 以其他字符拿到对应命名参数值, 需要先定义该变量, 否则将自动提升至全局(例如 keep, 在way1 外部还能访问)"
local alive k b c
aop caption center "4.1 不使用默认值, 则未出现的(例如 c )和未赋值的(例如 k)命名参数, 全部返回空"
_display alive k b c keep
aop caption center "4.2 使用默认值, 上述的 c 和 k 将区别对待"
_display_defaults alive k b c keep
aop caption center "4.3 仅使用一个默认值, 上述的 c 和 k 将返回统一默认值"
_display_single_default alive k b c keep
pushee
set +e
info "5. 测试参数名, 变量名的合法. 需要查看日志"
local slim_walk_new
param slim-walk-new
param polo-maya
param -arabal
param -abc*wf
popee
}
way2(){
info "不先调用 eval \$PASX, 直接 param, 提示错误, 并记录到日志"
param alive
}
way3(){
# params 函数测试
eval $PASX
param_look_up
info "1. 无参数, 则获取所有位置参数"
local arr # 声明普通数组变量
readarray -t arr <<< $(echox -e "$(params)")
describe_array "....." arr
info '2. 指定序号, 查询相应的位置参数:'
local oligan mini_slk no_such
params "2 oligan" "3 mini_slk" "5 no_such"
tip -e "result:<$oligan>,<$mini_slk>, <$no_such>"
info "3. 单参数查询"
local keep_kity_small b
params "'keep_kity-small' 'hello one' 'tree world'" "b 'blood you' 'scrash ere'"
describe_array keep_kity_small keep_kity_small
describe_array b b
info "4. 双参数查询"
local snake_climb_there
params "'-snake-climb_there alive' 999 888" "' slamp b' spring summer"
describe_array "-snake-climb_there & alive" snake_climb_there
local alived
params "'alived balive-fe' ' this is 123 how' 'fiei 456'"
describe_array "alivef..." alived
info "5. 综合查询"
local b k keyboard keep_kity_small
params "'keep-kity-small keep_kity-small' 659 324" "b" "k slap no-mark" "keyboard hehehe 893"
describe_array b b
describe_array k k
describe_array keyboard keyboard
describe_array keep_kity_small keep_kity_small
local keep_kity_small
params "2 keep_kity_small"
echox -e "第 2 个位置参数: <$(param 2 )>, 或者: <$keep_kity_small>"
info "2. 以变量名而不是数组名提取, 默认为第一个元素(如果有)."
tip "alive=<$alive>, k=<$k>, c=<$c>, keep=<$keep>"
info "3. 类似 1, 批量获取命名参数, 但同时指定必要的默认值"
params "k 'def1 for k' ' def2 for k' 'no use value...' " \
alive \
-5 \
b \
'c "def1 for c " "def2 for c "' \
123 \
8 \
keep
describe_array "items of alive" alive
describe_array "items of k" k
describe_array "items of c" c
describe_array "items of keep" keep
}
way4(){
info "测试在多处指定分隔符, 每一次指定均有确定的值, 以最后一次为准"
eval $PASX
param_look_up
info "SEP is <$__sep__>"
}
way5(){
info "测试在多处指定分隔符, 且最后的一个指定是空则意味着取消设置, 然后 SEP 又被配置为默认值."
eval $PASX
param_look_up
info "SEP is <$__sep__>"
}
#way1 one '\ttwo end ' -alive 'three 3 ' --keep 1989 -k -b four -alive "five six" ----SEP opq -b 'seven eight' --A+b -SEP abcde --A-c --A_d ' 9 nine ten 10' --alive "eleven twelve" -ank ------SEP
#notice "way1 外部访问, alive 因定义局部性而为空:<$alive>, keep 因默认为全局而保持: <$keep>"
#way2 one '\ttwo end ' -alive 'three 3 ' -snake-climb_there 'para nick name' -k -b four -alive "five six" -b 'seven eight' ' 9 nine ten 10' --alive "eleven twelve"
#way3 one '\ttwo end ' -alive 'three 3 ' --keep-kity_small sunday --keep_kity-small "sweet a day" -k -keep_kity-small 'saturday 中文 friday ' -b four -alive "five six" -b 'seven eight' ' 9 nine ten 10' --alive "eleven twelve"
# way4 -SEP 'AMK' -ank -SEP bcad
# way5 -SEP 'AMK' -ank -SEP bcad -SEP
way1..5, 逐个取消注释, 可以看到效果. 具体过程略.