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
原文地址:https://www.cnblogs.com/lookbackinside/p/2552087.html