shell 的选项解析
引言
目前在做嵌入式开发,经常要把程序 tftp 到设备上调试运行,打算写个脚本简化这些步骤,但系统所带 busybox 还是老旧的1.01版,不少 shell 特性都不支持,如 getopts。无奈,就写个老旧的脚本顶上吧~
目标
1. 脚本支持选项配置,如 -xfd /root/sbin app1 app2 .68,即,把文件 app1,app2 从 192.168.0.68 主机上传到设备路径 /root/sbin下,然后后台执行
2. 支持二级选项,如 -Lef,即,记录日志到 stderr 或 file
3. 可复用性,方便 copy 到别处用;可扩展性,方便增删选项支持;(所以尽量把各步骤封装成函数模块了);尽量使用内置命令
4. 用来以后复习 shell 常用语法
#!/bin/sh
### 功能 ### ## Tftp files from host to device ## wrap tftp functions with some handy options ## able to work with busybox 1.01 (ash) or above ### 默认配置 ### rda="192.168.0.128" ## 主机 ip rdir="" ## 目标文件夹 run=0 ## 是否运行 fork=0 ## 是否后台运行
DEBUG=1 ## 调试开关 #### 用法 #### if [ $# -le 0 ] || [ $1 = '?' ]; then echo "Usage: ${0##*/} [option] files_to_upload [.host_ip_tail]" echo "option: -d [dest_path] 移动文件至目的路径" echo " -x 运行" echo " -f 后台运行" echo "default: ${0##*/} files $rda" exit 1 fi ##############
实现
1. 处理主流程
先把参数中的各选项识别出来,然后分别处理,最后处理剩余参数
标注说明:红粗字体的为函数,高亮部分为复用及扩展时需要修改的部分
## 选项解析函数 parse_opt() { while local arg="$1" [ -n "$arg" ] ## 遍历命令行参数 do local oargs ## 保存非选项参数 local shift_step=1 ## 参数左移步进,用来遍历参数 case $arg in ## case 支持通配符(Globbing),可用来做参数匹配 -* ) ## 1.选项匹配 local opt="${arg#-}" opt_need_arg $opt ## 看看选项是否需要参数 if [ $? -gt 0 ]; then let shift_step++ ## 假设选项只支持带一个参数 local the_arg="$2" ## 所带参数 handle_opts $opt "$the_arg" ## 选项处理函数 else handle_opts $opt ## 选项处理 fi ;; .[0-9]* ) ## 2.特殊参数
local tail=${arg#?} if [ $tail -le 255 ]; then ## host ip rda="192.168.0.$tail" else echo "Error: invalid host ip!" && exit 7 fi ;; * ) ## 3.其它参数 oargs="$oargs $arg" dmsg "oargs=$oargs" ## dmsg 为调试函数 ;; esac dmsg "shift_step=$shift_step > $# ?" shift $((shift_step)) ## 命令行参数左移 done handle_others $oargs ## 最终参数处理函数 return 0 }
因为 shell 中变量作用域默认是全局的,所以函数中变量都作了 local 声明,方便复用
2. 选项处理函数
opt_need_arg() ## 返回选项所支持的参数个数 { dmsg "opt_need_arg $*" [ $(expr index "$1" d) -gt 0 ] && return 1 ## 选项 d 需要 1 个参数 return 0 } handle_opts() { dmsg "handle_opts $*" local opts="$1" ## 选项字符串列表,如 xfd local sub_pos=2 ## 选项子串位置 while local c1=$(expr substr "$opts" 1 1) ## 得到列表中第一个字符 [ -n "$c1" ] do dmsg "c1=$c1" case $c1 in L | l ) local bopts="ef" ## 二级选项 chk_bind_opt "${opts#?}" $bopts ## 二级选项检查函数,返回当前二级选项个数 local ret=$? if [ $ret -le 0 -o $ret -gt ${#bopts} ]; then ## 错误处理:若返回值为0,或超出所支持的选项个数 echo "Error: wrong use of -$c1!" exit 11 ## 返回值比较随意。。 else let sub_pos+=$ret ## 选项左移步进 dmsg "sub_pos=$sub_pos" [ $(expr index "$opts" f) -gt 0 ] && log2f=1 ## 二级选项处理
[ $(expr index "$opts" e) -gt 0 ] && log2e=1 ## 二级选项处理
fi ;; d ) if [ -z "$2" ] || [ "$opts" != "$c1" ]; then ## 错误处理:若选项需要参数,则该选项必须位于列表末位
echo "Error: wrong use of -$c1!" exit 6 else rdir="$2" fi return 0 ;; f ) fork=1;; x ) run=1;; * ) echo "Error: unkown opt \"-$c1\"!" exit 2 ;; esac opts="$(expr substr "$opts" $sub_pos ${#opts})" ## 选项字符串左移 sub_pos=2 dmsg "opts=$opts" done return 0 }
chk_bind_opt() ## 二级选项检查函数,主要用来查错 { dmsg "chk_bind_opt $1 $2" local opt2chk="$1" local optlist="$2" local num=0 while local c1=$(expr substr "$opt2chk" 1 1) [ -n "$c1" -a $(expr index "$optlist" $c1) -gt 0 ] do dmsg "c1=$c1" let num++ opt2chk="${opt2chk#?}" done dmsg "num=$num" return $num ## return see -1 as option }
3. 最后的自定义参数处理函数
handle_others() {
path="$(pwd)" ## 默认上传到当前路径
if [ -n "$rdir" -a "$path" != "$rdir" ]; then
path="$rdir"
mv=1
fi
for file in $* ## 文件挨个处理 do dmsg $file echo "tftp -gr $file $rda ..." if tftp -gr $file $rda; then chmod a+x $file if [ "$mv" -eq 1 ]; then echo "moving $file to $path ..." mv $file $path ## 其实 mv 支持批量移动 fi if [ $run -gt 0 ]; then
killall -q $file [ $fork -gt 0 ] && ("$path/$file" &) || "$path/$file" ## if-else 的简写形式 fi echo else exit 21 fi done }
4. 调试函数
dmsg() { [ -n "$DEBUG" ] && echo "$1" ## 若 DEBUG 有值则打印参数 1 }
5. 跑起来
parse_opt "$@" ## 看上去孤独了点,可以考虑解除其函数封装 exit 0
总结
1. 特殊参数的处理,可以考虑移到 oargs 的首部,从而在 handle_others() 中优先处理,省的污染 parse_opt();二级选项的处理没做,也许以后用得上
2. 不支持 basename 命令,用 ${0##*/};不支持${string:position:length} 提取子串,用 expr substr $string $position $length
3. 一些技巧:while 可以有多个判断条件,但根据最后一个做决定;if-else 的列表形式,有时比较简洁方便;case 支持通配符
参考文献:
[1]. Mendel Cooper,杨春敏、黄毅(译),高级 Bash Shell 编程指南,2006-05. [注]:本文所有技巧均源于此书
附录
## Usage of tftp in busybox 1.01 ## ## tftp [option] HOST [port] ## option: -g Get file ## -p Put file ## -l FILE Local file ## -r FILE Remote file ## -b SIZE