博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Bash Cookbook 学习笔记 【高级】
阅读量:7304 次
发布时间:2019-06-30

本文共 17513 字,大约阅读时间需要 58 分钟。

图片描述

Read Me

  • 本文是以英文版<bash cookbook> 为基础整理的笔记,力求脱水
  • 【高级】部分,涉及脚本安全、bash定制、参数设定等高阶内容
  • 本系列其他两篇,与之互为参考

    • 【基础】内容涵盖bash语法等知识点。
    • 【中级】内容包括工具、函数、中断及时间处理等进阶主题。
  • 所有代码在本机测试通过

    • Debian GNU/Linux 9.2 (stretch)
    • GNU bash, version 4.4.12(1)-release (x86_64-pc-linux-gnu)
  • 2018.02.03 更新 【七】编写安全的脚本.输入验证

约定格式

# 注释:前导的$表示命令提示符# 注释:无前导的第二+行表示输出# 例如:$ 命令 参数1 参数2 参数3 # 行内注释输出_行一输出_行二    $ cmd par1 par1 par2 # in-line commentsoutput_line1output_line2

七、编写安全的脚本

安全是一个过程,而不是某种成品、对象、或技术,且没有终点。 -- Bruce Schneier

比如:

  • 没有权限提升的隐患
  • 不会意外执行rm -rf /这样的破坏性代码
  • 不会泄露密码等敏感信息
  • 运行中断时清理现场 (fail gracefully)
  • 对用户的错误输入有容错和检查
  • 只使用可信赖的外部文件
  • 代码简洁、可读性强,文档完善,功能明确

当然,以上也适用于所有的软件。

展开细说,先从脚本头#!开始

shebang !

shebang#!出现在任何脚本的第一行,它告诉内核,该用什么解释器来处理该文件。

#!/bin/bash

同时,内核也会接收解释器(比如bash)后边跟的一个唯一参数(如果用户提供的话)。该参数会被利用来进行解释器欺骗(Interpreter Spoofing)。

所以,最好在该位置用减号-占位

#!/bin/bash -

但这样的路径/bin/是硬编码的,又会产生各OS之间可移植性的问题。

解决方法:通过原生的env命令,自动识别bash的安装位置

$ env...SHELL=/bin/bash...

所以,这样写行了吗?

#!/usr/bin/env bash -

还是不行。文件找不到了。

/usr/bin/env: ‘bash -’: No such file or directory

linux和很多其他unix的env,不允许后边跟两个或以上的参数,这里参数指的是bash-。BSD和Solaris等极少数除外。

所以,可移植和安全性有点鱼和熊掌的意思,需要自己权衡轻重

# 轻安全,重移植#!/usr/bin/env bash# 重安全,轻移植#!/bin/bash -

最后,再提一个细节。有时候,你会看到有些脚本的#!/bin/解释器之间有一个空格。这是为了向前兼容。很老的系统里需要这个空格。现在的话,可写可不写了。

#! /bin/bash

安全路径 $PATH getconf

shebang之后,写所有其他代码之前,请先设置安全路径

  • 第一种写法:

显式声明一遍PATH变量,并再次注册到运行环境。反斜杠用于禁用别名扩展功能。

PATH='/usr/local/bin:/bin:/usr/bin'\export PATH
  • 另一种方法:
export PATH=$(getconf PATH)

getconf用于获取系统参数设置

$ getconf -a | grep PATHPATH_MAX                           4096_POSIX_PATH_MAX                    4096PATH                               /bin:/usr/binCS_PATH                            /bin:/usr/bin

但第二种写法还存在个问题:$(变量)移植性不如单引号``。

而且,变量声明和export注册放在一条语句内也不是通用的写法

# 移植性不好export var='foo'# 最好拆开来写var='foo'; export var
  • 第三种方法:

既然注册$PATH路径变量的目的是为了查找工具,那么,可不可以直接指定各工具的路径呢?

这样写脚本会很长。所以最好打包进一个函数内,供各脚本调用。

#!/usr/bin/env bash# 工具查找# 复制、移动、删除,每个系统都一样_cp='/bin/cp'_mv='/bin/mv'_rm='/bin/rm'# 分支判断case $(/bin/uname) in    'Linux')        _cut='/bin/cut'        _nice='/bin/nice'        # [其他工具]    ;;    'SunOS')        _cut='/usr/bin/cut'        _nice='/usr/bin/nice'        # [其他工具]    ;;    # [其他系统环境]esac

当前路径 ./

为了减少输入量,有些用户习惯把当前路径.或空路径(尾部:,或中间::),也加进$PATH变量。

# 当前路径PATH=.:$PATHPATH=$PATH:.# 空路径PATH='/bin:/usr/bin:'PATH='/bin::/usr/bin'

从安全的角度,这是很不好的习惯,尤其是对root账户。

因为对命令进行路径搜索是按$PATH各项依次查找的。

当前路径在搜索链的存在会造成一些不可控的结果。

  • 设想一种情况:

如果当前路径放在最前

$ PATH='.:/bin:/usr/bin'; export PATH

此时在tmp目录执行ls时,系统会先尝试运行/tmp/ls,而这个ls如果意外存在的话,极可能是木马命令。

$ cd /tmp; pwd/tmp$ ls# 此处中招了
  • 再假设一种情况:

点号.放在最后

$ PATH='/bin:/usr/bin:.'; export PATH

你机子上恰好装有一款叫midnight commander程序,它的命令恰好是mc。你在移动文件时mv不小心写成了mc。本该执行的/bin/mv变成了./mc

$ PATH='/bin:/usr/bin:.'; export PATH$ mc file1 file2# 此处再次中招

以上两个例子有点极端。但所谓安全,不正是预防此类小概率事件吗?

禁用别名

恶意的别名类似木马(trojan),可以诱导用户执行不安全的命令。

看个简单的例子。

$ alias unalias=echo$ alias builtin=ls$ builtin unalias vils: unalias: No such file or directoryls: vi: No such file or directory$ unalias -a-a

通过使用别名,原生的builtinunalias都被其他命令覆盖了。

删除所有的别名,可以消除隐患。

\unalias -a

敏感信息

哈希表 hash

当前运行环境下,执行过的命令会被添加到哈希表(hash),用于提高再次调用时的访问速度。

污染(poison)哈希表

# dog指向cat$ hash -p /bin/cat dog$ hash -lbuiltin hash -p /bin/cat catbuiltin hash -p /bin/cat dogbuiltin hash -p /bin/stty sttybuiltin hash -p /usr/bin/clear clear

-r开关可用于清空哈希表

# 清理命令路径下的所有哈希值hash -r

核转储 core dump

core dump也被译为内核转储或核心转储,这里的内核有别于操作系统内核(kernel)

  • core : 应用程序在崩溃瞬间的内存等运行环境的快照,用于调试和分析
  • kernel : Linux系统最核心的那部分代码

被转储的内存页面可能含有密码等信息,最好禁用该功能。

且最好是写入系统级的配置文件中,如/etc/profile~/.bashrc

# 禁用脚本和相关进程的内核转储功能 可参考`man 1 bash`的相关章节ulimit -H -c 0 --# -H    硬上限# -c 0  核转储大小限制为0,即禁用

明文密码

首先一点,千万千万不要像这样写

$ ./某脚本 -u 用户 -p 密码 &[1] 13301

就算输入密码时,不回显到屏幕,也不行

read -s -p "password: " PASSWD;

因为,以参数形式传递给脚本的密码,始终是以明文的形式存在,通过ps进程列表,或以核转储的形式一览无余

$ ps  PID TTY          TIME CMD 2348 pts/1    00:00:00 bash 9661 pts/1    00:00:00 ps13301 pts/1    00:00:00 ./某脚本 -u 用户 -p 密码 &

如果避免不了要使用明文密码,可以单独放进其他用户没有查看权限的文件中

$ ./某问题脚本 ~.隐藏目录/密码文件

像这样间接引用,至少避免明文暴露的问题。

crypt或其他密码哈希可行吗?

首先,哈希是不可逆的,你无法还原回原来的明文。也就是无法访问那些需要该明文密码的数据库。如此,你只能取消数据库的密码保护,有点得不偿失。

哈希给你的,只是一种"安全"的假象。还不如用明文。

对于明文,一种简单的防护措施,可以是的形式,这个在前边介绍过。或用47个字符的扩展版本,除了大小写26个字母外,还支持标点。

$ ROT13=$(echo password | tr 'A-Za-z' 'N-ZA-Mn-za-m')$ ROT47=$(echo password | tr '!-~' 'P-~!-O')

这种打乱字母顺序的方式,有总比没有好点,至少不会让你产生"安全"的假象。

比以上更好的,是sudo,或SSH加密会话。后边再展开来谈。

文件权限 rwxrwxrwx

默认掩码 umask

umask是bash原生的命令,通过掩码改变创建文件(包括目录)时的默认权限。

用户 其他 八进制
原来的默认权限 rwx rwx rwx
二进制 111 111 111 777
掩码位 001 011 011 133
掩码后默认权限 110 100 100 644
rw- r-- r--
# 注意:该设置对命令行已被重定向的文件不会产生影响# 设置成变量形式,便于根据需要修改UMASK=002umask $UMASK

侦测外部可写目录 【脚本】

外部可写(world writable)目录,是任何其他用户都有可写权限的目录。当然,你肯定不希望此类权限出现在根用户的$PATH中。

最好能有个脚本,能检查指定路径下,此类不安全的目录是否存在。运行效果类似这样:

$ ./chkpath.sh; echo $?ok        drwxrwsr-x root staff /usr/local/binok        drwxr-xr-x root root /usr/binok        drwxr-xr-x root root /binok        drwxrwsr-x root staff /usr/local/gamesok        drwxr-xr-x root root /usr/games外部可写    drwxrwxrwt root root /tmp符号链接, ok        drwxr-xr-x root root /var/run缺失                /不存在的目录2$
#!/usr/bin/env bash# 统计异常目录个数exit_code=0#            列举所有需要检查的目录;for dir in ${PATH//:/ } /tmp /var/run /不存在的目录 ; do    # 如果是符号链接    [ -L "$dir" ] && printf "%b" "符号链接, "    # 如果不是目录    if [ ! -d "$dir" ]; then        printf "%b" "缺失\t\t\t\t"        (( exit_code++ ))    else        #    显示目录自身    |    取 [权限,用户,组]三列        stat=$(ls -lHd $dir | awk '{print $1, $3, $4}')        #                            其他用户可写        if [ "$(echo $stat | grep '^d.......w. ')" ]; then            printf "%b" "外部可写\t$stat "            (( exit_code++ ))        else            printf "%b" "ok\t\t$stat "        fi    fi    printf "%b" "$dir\n"doneexit $exit_code

该脚本的几个要点简单说明一下:

  • 变量切割

${PATH//:/ }将路径变量PATH/的冒号:替换为空格,格式${变量/分隔符/替换值}

用$IFS=':'的形式也能切割变量,但灵活性不如符号替换。

  • for循环

for循环用于实现路径遍历,它的明显优点是有很好的扩展性:

你可以添加任意目录进来

for dir in 目录1 目录2 ...; do    ...done

也可以在循环体内进行任意的条件测试

for dir in ...; do    [ -L "$dir" ] && ...    if [ ! -d "$dir" ]; then        ...    else        ...        if [ ... ]; then        ...    ...done
  • -d开关

ls -d表示只列出目录自身,不展示其中的内容。

$ echo ${PATH//:/ } | xargs ls -ldHdrwxr-xr-x 2 root  root   4096 Jan 21 08:22 /bindrwxr-xr-x 2 root  root  36864 Jan 21 08:23 /usr/bindrwxr-xr-x 2 root  root   4096 Jul 13  2017 /usr/gamesdrwxrwsr-x 2 root  staff  4096 Jul 24  2017 /usr/local/bindrwxrwsr-x 2 root  staff  4096 Jul 24  2017 /usr/local/games

更改权限 chmod

chmod用于修改目录及文件权限。

首先,权限可以有两种表现形式:

  • 4位八进制的绝对值
$ chmod 0755 some_script

很多人的习惯,是只使用后三位数。第一位是个特殊位,很少用到。但显式的写全四位能避免歧义。

  • 符号表示的相对值([ugo]+/-/=[rwx])
$ chmod -x some_script$ chmod ugo+rx some_script

相对值假设你知道原来的权限,带有主观性。绝对值不会造成误判,更保险一些。

修改完之后最好用ls -l再确认一遍。

关于批量修改:

-R递归形式是不建议的。它会将子目录都设为不可执行,这样,你就无法访问这些目录了。因为cd命令是需要可执行权限的

$ chmod -R 0644 some_directory

正确的写法,是对文件和目录区别对待,以find | xargs的组合方式进行批量修改

$ find some_directory -type f | xargs chmod 0644 # 文件$ find some_directory -type d | xargs chmod 0755 # 目录

创建新目录并设置权限,两个动作可以用一条命令完成,避免分开执行两条命令时,产生竞态(race condition)的隐患。

$ mkdir -m mode new_directory

批量修改权限前,你可能需要对整个系统或特定目录的权限设置先做备份。

备份文件系统的元数据 【脚本】

#!/usr/bin/env bash# 文件名 archive_meta.shprintf "%b" "权限\t用户\t组\t大小\t修改时间\t文件描述\n" > archive_filefind / \( -path /proc -o -path /mnt -o -path /tmp -o -path /var/tmp \-o -path /var/cache -o -path /var/spool \) -prune \-o -type d -printf 'd%m\t%u\t%g\t%s\t%t\t%p/\n' \-o -type l -printf 'l%m\t%u\t%g\t%s\t%t\t%p -> %l\n' \-o -printf '%m\t%u\t%g\t%s\t%t\t%p\n' >> archive_file

其中的(-path /foo -o -path ...) -prune句段用于排除不需要备份的路径。-printf进行格式化输出。效果如下:

$ sudo ./archive_meta.sh$ head archive_file 权限     用户     组      大小        修改时间                            文件描述d755    root    root    4096    Tue Oct 31 04:45:47.2825806270 2017    //d555    root    root    0        Fri Jan 26 06:35:45.5240001190 2018    /sys/d755    root    root    0        Fri Jan 26 06:35:45.5360001780 2018    /sys/kernel/...

这个脚本功能比较简单,只作为说明用。更专业的文件备份和完整性检查,可参考Tripwire等工具。

特殊权限 setuid setgid

在脚本中设置特殊位setuid (用户 user)和setgid (组 group),造成的混乱比解决的问题,要多得多。强烈不建议使用。

简单介绍一下。

  • 如何设置:

先分别创建两个普通的目录和文件

$ mkdir suid_dir sgid_dir; touch suid_file sgid_file; ls -ltotal 8drwxr-xr-x 2 jimhs jimhs 4096 Jan 26 11:58 sgid_dir/-rw-r--r-- 1 jimhs jimhs    0 Jan 26 11:58 sgid_filedrwxr-xr-x 2 jimhs jimhs 4096 Jan 26 11:58 suid_dir/-rw-r--r-- 1 jimhs jimhs    0 Jan 26 11:58 suid_file

四位权限绝对值的第一位数,4和2,就是setuid位和setgid位

$ chmod 4755 suid_dir suid_file$ chmod 2755 sgid_dir sgid_file

再次查看,已经设置好了。用户和组的x都变成了s

$ ls -ltotal 8drwxr-sr-x 2 jimhs jimhs 4096 Jan 26 11:58 sgid_dir/-rwxr-sr-x 1 jimhs jimhs    0 Jan 26 11:58 sgid_file*drwsr-xr-x 2 jimhs jimhs 4096 Jan 26 11:58 suid_dir/-rwsr-xr-x 1 jimhs jimhs    0 Jan 26 11:58 suid_file*
  • 测试是否已设置:

[ -u suid_dir ][ -g sgid_file ]用于对用户和组条件测试。

这两个值会改变创建和从属关系,导致不可控的权限泄漏。这也是造成混乱的源头。所以,没有关注就没有伤害~

隔离的环境

随机数 $RANDOM

在脚本运行环境,使用随机数命名的临时目录及文件,可以增加非法访问的难度。

最简单的随机数生成方式,是使用bash的内置变量${RANDOM}

$ echo ${RANDOM}${RANDOM}${RANDOM}68981103829905

07000600权限保证了其他用户没有访问权限。

# 随机临时目录until [ -n "$temp_dir" -a ! -d "$temp_dir" ]; do    temp_dir="/tmp/自定义前缀.${RANDOM}${RANDOM}${RANDOM}"donemkdir -p -m 0700 $temp_dir \    || { echo "FATAL: 无法创建临时目录'$temp_dir': $?"; exit 100 }# 随机临时文件temp_file="$temp_dir/自定义前缀.${RANDOM}${RANDOM}${RANDOM}"touch $temp_file && chmod 0600 $temp_file \    || { echo "FATAL: 无法创建临时文件'$temp_file': $?"; exit 101 }# 退出前记得删除临时目录cleanup="rm -rf $temp_dir"trap "$cleanup" ABRT EXIT HUP INT QUIT

相比起马上要介绍的其他方法,${RANDOM}虽然只能生成包含数字的随机数,但脚本写起来结构简单,简单意味着健壮。移植性好。

也可以这样生成随机数:

$ echo $( (last; who; free; date; echo $RANDOM) | md5sum | cut -d' ' -f1 )c0b5676e55987de62432117842247286

即,将一组无规律的命令打包,然后将结果进行哈希,再从中取出特定字段来作为随机数。这样做有点取巧,只是提供一种思路。

更专业的实现方式,当然是使用mktemp/dev/urandom,但考虑到不是任何系统都支持,为了保证脚本的健壮性,避免不了各种繁琐的验证和错误处理。

创建安全的临时目录或文件 【脚本】

# 调用方法: #   $temp_file=$(MakeTemp 
[path/to/name-prefix])# 示例:# $temp_dir=$(MakeTemp dir /tmp/$PROGRAM.foo)# $temp_file=$(MakeTemp file /tmp/$PROGRAM.foo)function MakeTemp { # 首先,确保$TMP变量已设置 [ -n "$TMP" ] || TMP='/tmp' local temp_type='' local sanity_check='' # 类型 file或dir local type_name=$1 # 如果未指定前缀,则使用$TMP + temp local prefix=${2:-$TMP/temp} case $type_name in file ) temp_type='' ur_cmd='touch' # 条件测试: 是常规文件、可读、可写、只有我有访问权限 sanity_check='test -f $TEMP_NAME -a \ -r $TEMP_NAME -a \ -w $TEMP_NAME -a \ -O $TEMP_NAME' ;; dir|directory ) temp_type='-d' ur_cmd='mkdir -p -m0700' # 条件测试: 是目录、可读、可写、可执行、只有我有访问权限 sanity_check='test -d $TEMP_NAME -a \ -r $TEMP_NAME -a \ -w $TEMP_NAME -a \ -x $TEMP_NAME -a \ -O $TEMP_NAME' ;; * ) Error "\n$PROGRAM:MakeTemp 参数错误! file或dir." 1 ;; esac # 先试下mktemp TEMP_NAME=$(mktemp $temp_type ${prefix}.XXXXXXXXX) # 失败的话,则用urandom if [ -z "$TEMP_NAME" ]; then TEMP_NAME="${prefix}.$(cat /dev/urandom | od -x | tr -d ' ' | head -1)" $ur_cmd $TEMP_NAME fi # 看下创建好没有,没有的话只能退出了 if ! eval $sanity_check; then Error "\a致命错误: 无法创建$type_name with '$0:MakeTemp $*'!\n" 2 else echo "$TEMP_NAME" fi } # MakeTemp函数结束

受限控制台 rbash

rbash即功能受限的控制台(restricted bash),比如不允许cd到其他目录、不允许改变环境变量等。具体请参考man rbash

使用前,需要做些必要配置:

  • /etc/passwd为特定用户绑定rbash,比如访客账户等
  • viemacs等可以越权访问到系统根路径的危险程序,全部禁用
  • 安全命令,放入专门的目录;$PATH唯一绑定到该目录

硬币的另一面:一些实用的程序被禁用后,肯定也影响到使用体验。而且,总会有漏网之鱼。所以,rbash也不是绝对安全的,只不过是门上多了一道锁。

监狱 chroot

没错,这个是叫监狱(jail)。

很好理解,就是把那些可疑的脚本或程序,用chroot关进监狱,坏脚本就算要搞破坏,影响也是可控的。

类似于构建了一道隐形的围墙,chroot会把根路径/绑定到指定的安全目录(change root)。该目录的父节点对里边的程序是不可见的。结合前一节提到的rbash,很多原本视为“危险”的程序,就没必要再被禁用了。

但有些程序,天生需要被暴露给外边的网络,比如各种DNS、HTTP或邮件服务器等。功能越复杂,管理成本也越高。

扩展阅读,可参考wiki上关于强制访问控制的介绍。

权限提升 sudo

sudo允许授权用户临时获得root账户权限。

使用前请先花点时间学习该命令、授权配置工具visudo/etc/sudoers文件(man sudoers)。

类似ALL=(ALL) ALL的授权滥用,会架空系统的整套防御机制。

查看用户授权

$ sudo -l

查看sudo的详细设置

$ sudo sudo -V | less

sudo批量命令时,这样写是错的。因为sudo只能影响到它后边的第一个参数。

sudo 命令1 && 命令2 || 命令3

正确的写法

$ sudo bash -c '命令1 && 命令2 || 命令3'

能用sudo的地方,就不要使用su

输入验证

所谓验证,就是定义一种模式,然后将用户输入与之比较,结果无外乎两种,要么匹配,要么不匹配

常用的句法结构,可以是简单的一条语句

[模式] && 执行

复杂点的,可以是庞大的分支结构

case    模式1) 执行1 ;;    模式2) 执行2 ;;    ...esac

这些在前边基础部分的都已经都介绍过了。

本节着重讲如何定义验证模式,及如何拆解用户提供的选项和参数。并结合一些实例,来强化学习。

最简单的匹配语法,是像这样:

[ 文件名 == *.jpg ] && echo "是jpg文件"# 模式不要用括号包裹。否则会被理解为字符本身[ 文件名 == "*.jpg" ] && echo "是jpg文件"

在这里,星号*还是作为通配符使用,不要与正则表达式搞混了。

简单匹配 【简表】

类型 匹配方式
* 任意字符串,包括null
? 任意单字符
[ ... ] 匹配括号内的任意字符
[ !... ] 不匹配括号内的任意字符
[ ^... ] 不匹配括号内的任意字符

简单匹配 【脚本】

bash安装包的examples路径下,给出了一些输入验证的示范代码。

  • 带正负号的数字验证
#examples/functions/isnum2# 整数isnum2(){    case "$1" in    '[-+]' | '')    return 1;;    # 为空,或只有正负号    [-+]*[!0-9]*)    return 1;;    # 有正负号,但不是数字    [-+]*)        return 0;;    # OK    *[!0-9]*)    return 1;;    # 不是数字    *)        return 0;;    # OK    esac}# 浮点数isnum3(){    case "$1" in    '')        return 1;;    # 为空    *[!0-9.+-]*)    return 1;;    # 非数字、正负号或小数点    *?[-+]*)    return 1;;    # 符号不是首位    *.*.*)        return 1;;    # 小数点超过一个    *)        return 0;;    # OK    esac}
  • ip地址验证
# examples/functions/isvalidipis_validip(){    case "$*" in    ""|*[!0-9.]*|*[!0-9]) return 1 ;;    esac    local IFS=.    # 以.作为分隔符    set -- $*      # 将参数分隔后映射到位置变量        [ $# -eq 4 ] &&        [ ${1:-666} -le 255 ] && [ ${2:-666} -le 255 ] &&        [ ${3:-666} -le 255 ] && [ ${4:-666} -le 254 ]}

bash 2.0之后,引入了双括号[[ ]],用以支持更复杂的匹配语法,并从视觉上区别于老式的单括号[ ]

其中的双等号==也可写为=,但建议用前者。

# 启用**扩展匹配**(extended globbing)shopt -s extglob# 对大小写不敏感shopt -s nocasematch# 匹配次数(关键字1|关键字2)if [[ 文件名 == *.@(jpg|jpeg) ]]then    # ...

扩展匹配 【简表】

类型 匹配次数
@( ... ) 一次
*( ... ) 零或多次
+( ... ) 一或多次
?( ... ) 零或一次
!( ... ) 不要匹配

如果扩展匹配还是不能满足要求,就该正则表达式(以下简称regex)出场了。

在中级部分讲grep工具时,已经介绍过一些常用语法。

其他工具,比如gawksed、或是vim等编辑器内,都支持regex语法,但对于bash自身而言,唯一一处会用到regex的地方,就是在[[ ]]这样的测试语句中。此时,双等号==要改为=~,以区别于简单和扩展匹配的语法。

[[ 文件名 =~ [[:alpha:]]{3,6}\.jpg ]] && echo "是jpg文件"

方括号内的方括号,是POSIX字符集合,常用的包括:

[[: alnum :]] [[: graph :]] [[: word :]] [[: alpha :]] [[: ascii :]] [[: blank :]] [[: cntrl :]]
[[: digit :]] [[: lower :]] [[: print :]] [[: punct :]] [[: space :]] [[: upper :]] [[: xdigit :]]

复杂一点的例子。比如想用数字编号重命名CD曲目

$ lsLudwig Van Beethoven - 01 - Allegro.oggLudwig Van Beethoven - 02 - Adagio un poco mosso.oggLudwig Van Beethoven - 03 - Rondo - Allegro.oggLudwig Van Beethoven - 04 - "Coriolan" Overture, Op. 62.oggLudwig Van Beethoven - 05 - "Leonore" Overture, No. 2 Op. 72.ogg$

文件名的结构:

  • 带空格的字母集- 数字集 - 所有剩下的部分(曲目名称.后缀)

进一步抽象:

  • (regex1)- (regex2) - (regex3)

所以,最终的regex表达式:

  • ([[:alpha:][:blank:]]*)- ([[:digit:]]*) - (.*)$

三个圆括号包裹的子表达式,被映射到内置变量BASH_REMATCH数组中,数组第0项表示整条regex语句,其他分别按1、2、3等一一对应。它也是一个内置变量。

for CDTRACK in *do    if [[ "$CDTRACK" =~ "([[:alpha:][:blank:]]*)- ([[:digit:]]*) - (.*)$" ]]    then        echo Track ${BASH_REMATCH[2]} is ${BASH_REMATCH[3]}        mv "$CDTRACK" "Track${BASH_REMATCH[2]}"    fidone

选项与参数 getops $OPTIND $OPTARG

选项(option)有两种。

一种不带参数(argument),类似于一个开关,通过打开或关闭,来改变脚本的行为

# 分开$ ls -a -l -h...# 合并$ ls -alh...

另一种要带参数

$ mysql -u 用户名

除此之外的,都被视为非选项参数

以上介绍了四个概念,用个完整的例子来演示:

myscript -a -b alt plow harvest reap

其中:

  • 开关选项 -a
  • 带参选项 -b
  • 选项参数 alt
  • 非选项参数 plow harvest reap

在脚本中,如何接收和验证这些选项和参数?

先贴答案:

#!/usr/bin/env bash#getopts.shaflag=bflag=while getopts 'ab:' OPTIONdo    case $OPTION in    a)        aflag=1        ;;    b)        bflag=1        bval="$OPTARG"        ;;    ?)        printf "用法: %s: [-a] [-b value] args\n" $(basename $0) >&2        exit 2        ;;    esacdoneshift $(($OPTIND – 1))if [ "$aflag" ]then    printf "选项 -a 已提供\n"fiif [ "$bflag" ]then    printf '选项 -b "%s" 已提供\n' "$bval"fiprintf "剩下的参数是: %s\n" "$*"

脚本的核心部分是:

getopts 'ab:' OPTION

内置命令getopts,用于接收以减号-开头的选项。每接收到一个,就放入OPTION变量中,用于后续处理,并返回TRUE。这样,wihle循环到下一圈。如此反复,直至所有选项被耗尽(取完),或遇到两个减号--,这时,返回FALSEwhile循环终止。

getopts可接受的选项范围在单引号中定义,这里是ab。冒号:表示b是带参选项。如果a是带参选项,则写为'a:b'。选项参数会被放入内置变量$OPTARG中。

while循环之后的下一条语句是

shift $(($OPTIND – 1))

$OPTIND内置变量用于存放选项和参数的位置索引,初始值是1。每执行一次getopts,该值递增并指向下个待处理选项。

所以,以下命令在while循环停止的时候,$OPTIND数值是4,指向"plow"的位置。也即通过shift右移3次(3=4-1)达到。

myscript -a -b alt plow harvest reap位置参数   1  2  3   4

脚本中,$*用于取完所有剩下的非选项参数"plow harvest reap"

printf "剩下的参数是: %s\n" "$*"

运行效果:

./getopts.sh -ab alt plow harvest reap选项 -a 已提供选项 -b "alt" 已提供剩下的参数是: plow harvest reap

自定义错误 【脚本】

对于非法选项,getopts会提供默认的错误警告信息。如需关闭,可先设置OPTERR=0。

如需使用自定义的错误警告,则在getopts定义选项接收范围时,在最开头的位置用冒号:标识。

getopts ':ab:' OPTION

增加了自定义错误警告的脚本:

#!/usr/bin/env bash#getopts.shaflag=bflag=# printf "OPTIND: %d\n" $OPTIND#OPTERR=0while getopts :ab: FOUNDdo    # printf "OPTIND: %d\n" $OPTIND    case $FOUND in    a)        aflag=1        ;;    b)        bflag=1        bval="$OPTARG"        ;;    \:)    # 反斜杠\表示取消对冒号转义,下同        printf "%s 选项缺少参数\n" $OPTARG        printf "用法: %s: [-a] [-b value] args\n" $(basename $0)        exit 2        ;;    \?)        printf "未知选项: -%s\n" $OPTARG        printf "用法: %s: [-a] [-b value] args\n" $(basename $0)        exit 2        ;;    esac >&2doneshift $(($OPTIND - 1))if [ "$aflag" ]then    printf "选项 -a 已提供\n"fiif [ "$bflag" ]then    printf '选项 -b "%s" 已提供\n' "$bval"fiprintf "剩下的参数是: %s\n" "$*"

与前一个例子不同的几个地方:

  • 前导冒号: 当你输入的选项缺少参数、或选项未定义时,getopts会分别返回字面的冒号:或问号?。同时,该选项符号被放入$OPTARG变量,这样,就便于在定义错误警告的格式化语句中进行引用了。
  • 转义和不转义的区别: case分支中,冒号:前的反斜杠可写可不写。问号?前要写(即,不做转义)。两者都写,是为了保持一致,更美观。而前一个例子的问号前之所以不带反斜杠,是因为把它放在case语句的最后一条缺省分支中,既表示字面的?(也即getopts的返回值),也表示通配符扩展,用来匹配任意字符。
  • 重定向: 本例中,将整个case块都重定向到标准错误(STDERR 2),比前例每条printf语句单独重定向要更好维护。

运行效果:

./getopts.sh -a -bb 选项缺少参数用法: getopts.sh: [-a] [-b value] args
bash现在的主要维护者
Chet Ramey,在bash源代码目录下(examples/scripts/shprompt),给出了一个输入验证的完整模板。内容太长,这里不贴了。有兴趣的读者可以参考。

转载地址:http://qsanm.baihongyu.com/

你可能感兴趣的文章
adb命令启动展讯平台工厂模式
查看>>
ABI的合集
查看>>
linux scp远程拷贝文件及文件夹
查看>>
泊松分布与美国枪击案
查看>>
递归遍历某个文件夹下所有包含某类型的文件
查看>>
Apache随机出现403 Forbidden探析
查看>>
Eclipse背景颜色设置(设置成豆沙绿色保护眼睛,码农保护色)
查看>>
sql server触发器知识点储备
查看>>
exchange
查看>>
自定义菜单控制程序
查看>>
mysql的备份-完全备份
查看>>
SELinux笔记
查看>>
使用switch(true) 代替冗长的if-else
查看>>
摇摆了很久,还是选择了Fedora
查看>>
Perl/Tk练习
查看>>
spring接收json格式参数的post请求
查看>>
精通电子邮件的八个核心方法
查看>>
自定义UITextView 支持 placeholder SetToFit
查看>>
Lively TableView
查看>>
Ejecta
查看>>