shell高级编程笔记(第九章 变量重游)

    技术2023-08-07  69

    第三部分 超越基本

    第九章 变量重游

    如果变量使用恰当,将会增加脚本的能量和灵活性。但前提是这需要仔细学习变量的细节知识。

    9.1 内部变量

    $BASH 这个变量将指向Bash的二进制执行文件的位置

    echo $BASH #/bin/bash

    $BASH_ENV 这个环境变量将指向一个Bash启动文件,这个启动文件将在调用一个脚本是被读取

    $BASE_SUBSHELL 这个变量将提醒subshell的层次,这是一个在version3才被添加到Bash中的新特性。见Example 20.1

    $BASH_VERSINFO[n] 这个Bash安装信息的一个6元素的数组。与下边的$BASH_VERSION很像,但这个更加详细

    #Bash version info : for n in 0 1 2 3 4 5 do echo "BASH_VERSINFO[$n]=${BASH_VERSINFO[$n]}" done #BASH_VERSINFO[0]=4 #主版本号 #BASH_VERSINFO[1]=1 #次版本号 #BASH_VERSINFO[2]=2 #Patch次数 #BASH_VERSINFO[3]=1 #Build version #BASH_VERSINFO[4]=release #Release status #BASH_VERSINFO[5]=x86_64-redhat-linux-gnu #Architecture

    $BASH_VERSION 安装在系统上的Base的版本号

    echo $BASH_VERSION #4.1.2(1)-release

    $DIRSTACK 在目录栈中最上边的值(将受到pushd和popd的影响) #这个内建的变量与dirs命令是保持一致的,但是dirs命令将显示目录栈的整改内容

    $EDITOR 脚本调用的默认编辑器,一般是vi或是emacs

    $EUID "effective"用户ID号 #当前用户被假定的任何id号。可能在su命令中使用 #注意:$EUID并不一定与$UID相同

    $FUNCNAME 当前函数的名字

    xyz23(){ echo "$FUNCNAME now executing." #xyz23正在执行 } xyz23 echo "FUNCNAME=$FUNCNAME" #FUNCNAME= #出了函数就变为Null了。

    $GLOBIGNORE 一个文件名的模式匹配列表,如果在file globbing中匹配到的文件包含这个列表中的某个文件,那么这个文件将被从匹配的文件中去掉

    $GROUPS 当前用户属于的组

    $HOME 用户的home目录,一般都是/home/username(见Example 9.14)

    $HOSTNAME hostname命令将在一个init脚本中,在启动的时候分配一个系统名字 gethostname()函数将用来设置这个$HOSTNAME内部变量。(见Example 9.14)

    $HOSTTYPE 主机类型

    echo $HOSTTYPE #x86_64

    $IFS 内部域分割符 这个变量用来觉得Bash在解释字符串是如何识别域或者单词边界 $IFS默认为空白(空格,tab,新行),但可以修改,比如在分析逗号分隔的数据文件时。 注意:$IFS中的第一个字符,具体见Example 5.1

    [root@localhost aaa]# set a b c d [root@localhost aaa]# IFS="=" [root@localhost aaa]# echo "$*" a=b=c=d

    Example 9.1 $FIS和空白

    #!/bin/bash #$IFS处理空白的方法,与处理其他字符不同 output_args_one_per_line(){ for arg do echo "[$arg]" done } echo;echo "IFS=\" \"" echo "----------" IFS=" " var1="a b c " output_args_one_per_line $var1 echo;echo "IFS=:" echo "----------" IFS=: var2=":a::b:c:::" output_args_one_per_line $var2 echo exit 0 #[root@localhost ~]# sh test.sh # #IFS=" " #---------- #[a] #[b] #[c] # #IFS=: #---------- #[] #[a] #[] #[b] #[c] #[] #[]

    Example 12.37 也是使用$IFS的另一个启发性的例子。

    $IGNOREEOF 忽略EOF:告诉shell在log out之前要忽略多少文件结束符

    $LC_COLLATE 常在.bashrc或/etc/profile中设置,这个变量用来在文件名扩展和模式匹配校对顺序。 如果$LC_COLLATE被错误的设置,那么将会在filename globbing中引起错误的结果 注意:在2.05以后的Bash版本中,filename globbing将不再对[]中的字符区分大小写 比如:ls [A-M]* 将匹配File.txt也会匹配file1.txt。为了恢复[]的习惯用法,设置$LC_COLLATE的值为c,使用export LC_COLLATE=c在/etc/profile或者是~/.bashrc中

    $LC_CTYPE 这个内部变量用来控制globbing和模式匹配的字符串解释

    $LINENO 这个变量记录它所在的shell脚本中的行号。这个变量一般用于调试目的

    last_cmd_arg=$_ echo "At line number $LINENO,variable \"v1\" =$v1" echo "Last command argument processed=$last_cmd_arg"

    $MACHTYPE 系统类型;提示系统硬件

    [root@localhost aaa]# echo $MACHTYPE x86_64-redhat-linux-gnu

    $OLDPWD 老的工作目录(你所在的之前的目录)

    $OSTYPE 操作系统类型

    [root@zhhs aaa]# echo $OSTYPE linux-gnu

    $PATH 指向Bash外部命令所在的位置,一般为/usr/bin,/usr/X11R6/bin等 当给出一个命令时,Bash将自动对$PATH中的目录做一张hash表。$PATH中以:分隔的目录列表将被存储在环境变量中。一般的,系统存储的$PATH定义在/ect/processed或~/.bashrc中(见Appendix G)

    echo $PATH /bin:/usr/bin:/usr/local/bin:/usr/X11R6/bin:/sbin:/usr/sbin PATH=${PATH}:/opt/bin #将把/opt/bin目录附加到$PATH变量中。在脚本中,这是一个添加目录到$PATH中的便捷方法。这样在这个脚本退出的时候,$PATH将恢复

    #注意:当前的动作目录"./"一般都在$PATH中被省去

    $PIPESTATUS 数组变量将保存最后一个运行的前台管道的退出码。

    这个退出码和最后一个命令运行的退出码并不一定相同

    [root@localhost aaa]# echo $PIPESTATUS 0 [root@localhost aaa]# ls -al |ww -bash: ww: command not found [root@localhost aaa]# echo $PIPESTATUS 141 [root@localhost aaa]# ls -al | ww -bash: ww: command not found [root@localhost aaa]# echo $? 127

    $PIPESTATUS数组的每个成员都会保存一个管道命令的退出码,$PIPESTATUS[0]保存第一个,$PIPESTATUS[1]保存第二个,以此类推

    注意:$PIPESTATUS变量在一个login shell中可能会包含一个错误的0值(3.0一下的版本会有这个问题)

    who | grep nobody |sort echo ${PIPESTATUS[*]} 0

    包含在脚本中的时候会产生一个期望值 0 1 0

    注意:在某些上下文$PIPESTATUS可能不会给出正确的结果

    [root@localhost aaa]# echo $BASH_VERSION 4.1.2(1)-release [root@localhost aaa]# ls | ww | wc -bash: ww: command not found 0 0 0 [root@localhost aaa]# echo ${PIPESTATUS[@]} 141 127 0 #Chet Ramey把上边输出不正确的原因归咎于ls的行为。 #因为如果把ls的结果放到管道上,并且这个输出没有被读取,那么SIGPIPE将会kil掉它,并且退出码变为141,而不是我们期望的0.这种情况也会发生在tr命令中

    注意:$PIPESTATUS是一个不稳定变量。在任何命令插入之前,并且管道询问之后,这么变量需要立即被捕捉

    $PPID 一个进程的$PPID就是它的父进程的进程id(pid);可以使用pidof命令对比一下

    $PROMPT_COMMAND 这个变量保存一个在主提示符($PS1)显示之前需要执行的命令

    $PS1 主提示符,具体见命令行上的显示

    $PS2 第二提示符,当你需要额外的输入的时候将会显示,默认为">"

    $PS3 第三提示符,在一个select循环中显示(见Example 10.29)

    $PS4 第四提示符,当使用-x选项调用脚本时,这个提示符将出现在每行的输出前边。默认为"+"

    $PWD 工作目录

    #!/bin/bash E_WRONG_DIRECTORY=73 TargetDirectory=/home/bozo/projects/GreatAmericanNovel cd $TargetDirectory echo "Deleting stale files in $TargetDirectory." if [ "$PWD" != "$TargetDirectory" ];then #防止删错目录 echo "Wrong directory" echo "In $PWD,rather than $TargetDirectory" echo "Bailing out" exit $E_WRONG_DIRECTORY fi rm -rf * rm .[A-Za-z0-9]* #删除隐藏文件 rm -f .[^.]*..?* #删除以多个.开头的文件 echo "Done" echo "Old files deleted in $TargetDirectory" echo exit 0

    $REPLY read命令如果没有给变量,那么输入将保存在$REPLY中。在select菜单中也可以,但是只提供选择的变量的项数,而不是变量本身的值

    #!/bin/bash #repli.sh #REPLY是read命令结果保存的默认变量 echo -n "What is your favorite vegetable?" read echo "Your favorite vegetable is $REPLY" #当在没有变量提供给read命令时,REPLY才保存最后一个read命令读入的值 echo -n "What is your favorite fruit?" read fruit echo "Your favorite fruit is $fruit" echo "Value of \$REPLY is still $REPLY" echo exit 0

    $SECONDS 这个脚本已经运行的时间(单位:秒)

    #!/bin/bash TIME_LIMIT=10 INTERVAL=1 echo echo "Hit Control-C to exit before $TIME_LIMIT seconds" echo while [ "$SECONDS" -le "$TIME_LIMIT" ] do if [ "$SECONDS" -eq 1 ];then units=second else units=seconds fi echo "This script has been running $SECONDS $units" sleep $INTERVAL done echo -e "\a" exit 0

    $SHELLOPTS 这个变量保存shell允许的选项,这个变量是只读的

    [root@localhost aaa]# echo $SHELLOPTS braceexpand:emacs:hashall:histexpand:history:interactive-comments:monitor

    $SHLVL shell层次,就是shell层叠的层次,如果是命令行,那$SHLVL就是1,如果命令执行的脚本中,$SHLVL就是2,以此类推

    $TMOUT 如果$TMOUT环境变量被设置为一个非0的值,那么在过了这个指定时间后,shell提示符将会超时,这回引起一个logout。 在2.05版本的Bash中,已经支持一个带有read命令的脚本中使用$TMOUT变量

    #!/bin/bash TMOUT=5 echo "What is your favrite song?" echo "Quickly now,you only have $TMOUT seconds to answer" read song if [ -z "$song" ];then song="(no answer)" fi echo "Your favorite song is $song" exit 0

    这里有一个更复杂的方法来在一个脚本中实现超时功能。一种办法就是建立一个时间循环,在超时的时候通知脚本。不过,这也需要一个信号处理机制,在超时的时候来产生中断(参见Example 29.5)

    Example 9.2 时间输入

    #!/bin/bash #timed-input.sh TIMELIMIT=3 PrintAnswer(){ if [ "$answer" = TIMEOUT ];then echo $answer else echo "Your favorite veggie is $answer" kill $! #$! 是运行在后台的最后一个工作的PID fi } TimeOn(){ sleep $TIMELIMIT && kill -s 14 $$ & #等待3秒,然后发送一个信号给脚本 #$$是当前运行脚本的PID } Int14Vector(){ answer="TIMEOUT" PrintAnswer exit 14 } trap Int14Vector 14 #捕获的信号等于14时,执行Int14Vector函数 #示例:trap "commands" signal-list #意思为:接受到与signa-list清单中相同的信号时,执行""中的commands echo "What is your favorite vegetable" TimeOn read answer PrintAnswer exit 0

    Example 9.3 再来一个时间输入

    #!/bin/bash #timeout.sh INTERVAL=5 #timeout间隔 timedout_read(){ timeout=$1 varname=$2 old_tty_settings=`stty -g` stty -icanon min 0 time ${timeout}0 #min n和-icanon配合使用,设置每次一完整读入的最小字符数为<N> read $varname stty "$old_tty_settings" } echo;echo -n "What's your name? Quick!" timedout_read $INTERVAL your_name echo if [ ! -z "$your_name" ];then echo "Your name is $your_name" else echo "Timed out" fi echo exit 0

    stty

    stty 修改中断命令行的相关设置 语法: stty (选项) (参数) 选项: -a : 以容易阅读的方式打印当前的所有配置 -g : 以stty可读方式打印当前的所有配置 实例: 在命令行下,禁止输出大写:

    stty iuclc #开启 stty -iuclc #关闭

    在命令行下禁止输出小写:

    stty olcuc #开启 stty -olcuc #关闭

    屏蔽显示

    stty -echo #禁止回显 stty echo #打开回显

    忽略回车符:

    stty igncr #开启 stty -igncr #恢复

    改变Ctrl+D的方法: stty eof “string”

    定时输入:

    timeout_read() { timeout=$1 old_stty_settings=`stty -g` stty -icanon min 0 time 100 eval read varname stty "$old_stty_setting" }

    更简单的方法是:

    read -t 10 varname

    Example 9.4 Timed read

    #!/bin/bash #t-out.sh TIMELIMIT=4 read -p "input variable values:" -t $TIMELIMIT variable <&1 #Bash 1.x和Bash 2.x需要使用<&1,Bash 3.x不需要 echo if [ -z "$variable" ];then echo "Time out,variable still unset" else echo "variable = $variable" fi exit 0

    $UID 用户ID号

    注意:变量$ENV,$LOGNAME,$MAIL,$TERM,$USER,$USERNAME并不是Bash的内建变量。它们经常被设置成环境变量,它们一般都放在Bash的安装文件中。 $SHELL:登录用户的shell名字,可能是从/etc/passwd设置的,也可能是在一个init脚本中设置的,同样,他也不是Bash的内建变量

    位置参数

    $0,$1,$2,…等

    $# #命令行或者是位置参数的个数(见Example 33.2)

    $* #所有的位置参数,被作为一个单词 注意:"$*"必须被引用

    $@ #与$*同义,但是每个参数都是一个独立的"“引用字串,这就意味着参数被完整的传递 注意:”$@"必须被引用

    Example 9.6 arglist:通过$*和$@列出所有的参数

    #!/bin/bash #arglist.sh #多使用几个参数来调用这个脚本,比如one tow three E_BADARGS=65 if [ ! -n "$1" ];then echo "Usage:`basename $0` argument1 argument2 etc" exit $E_BADATGS fi echo index=1 #初始化数量 echo "Listing args with \"\$*\":" for arg in "$*" #如果"$*"不被引用,那么将不能正常工作 do echo "Arg #$index = $arg" let "index += 1" done #$*认为所有的参数为一个单词 echo "Entire arg list seen as single word" echo #Listing args with "$*": #Arg #1 = one tow three #Entire arg list seen as single word index=1 #重置数量 echo "Listing args with \"\$@\":" for agr in "$@" do echo "Arg #$index = $agr" let "index += 1" done #$@认为每个参数都是一个单词 echo "Arg list seen as separate words" echo #Listing args with "$@": #Arg #1 = one #Arg #2 = tow #Arg #3 = three #Arg list seen as separate words index=1 #重置数量 echo "Listing args with \$*(unquoted):" for arg in $* do echo "Arg #$index = $arg" let "index += 1" done #未""引用的$*,把参数作为独立的单词 echo "Arg list seen as separate words" #Listing args with $*(unquoted): #Arg #1 = one #Arg #2 = tow #Arg #3 = three #Arg list seen as separate words exit 0

    在shift命令后边,$@将保存命令行中剩余的参数,而$1被丢掉了

    #!/bin/bash #使用./scriptname 1 2 3 4 5 来调用这个脚本 echo "$@" #1 2 3 4 5 shift echo "$@" #2 3 4 5 shift echo "$@" #3 4 5 #每个shift都丢弃$1,"$@"将包含剩下的参数 #$@也作为工具使用,用来过滤传给脚本的输入 #cat "$@"结构接受从stdin传来的输入,也接受从参数中指定的文件来输入

    注意:$*和$@的参数有时会不一致,发生令人迷惑的行为,这依赖于$IFS的设置

    Example 9.7 不一致的$*和$@行为(这个脚本看的人好乱)

    #!/bin/bash # #"$*"和"$@"的古怪行为,依赖于它们是否被""引用。单词拆分和换行的不一致处理 echo echo 'IFS 不变,使用"$*"' c=0 for i in "$*" do echo "$((c+=1)):[$i]" done echo "----------" echo 'IFS 不变,使用$*' c=0 for i in $* do echo "$((c+=1)):[$i]" done echo "----------" echo 'IFS 不变,使用"$@"' c=0 for i in "$@" do echo "$((c+=1)):[$i]" done echo "----------" echo 'IFS 不变,使用$@' c=0 for i in $@ do echo "$((c+=1)):[$i]" done echo "----------" IFS=: echo 'IFS=":",using "$*"' c=0 for i in "$*" do echo "$((c+=1)):[$i]" done echo "----------" echo 'IFS=":",using $*' c=0 for i in $* do echo "$((c+=1)):[$i]" done echo "----------" echo 'IFS=":",using "$@"' c=0 for i in "$@" do echo "$((c+=1)):[$i]" done echo "----------" echo 'IFS=":",using $@' c=0 for i in $@ do echo "$((c+=1)):[$i]" done echo "----------" var=$* echo 'IFS=":",using "$var"(var=$*)' c=0 for i in "$var" do echo "$((c+=1)):[$i]" done echo "----------" echo 'IFS=":",using $var(var=$*)' c=0 for i in $var do echo "$((c+=1)):[$i]" done echo "----------" var="$*" echo 'IFS=":",using "$var"(var="$*")' c=0 for i in $var do echo "$((c+=1)):[$i]" done echo "----------" echo 'IFS=":",using $var(var="$*")' c=0 for i in "$var" do echo "$((c+=1)):[$i]" done echo "----------" var=$@ echo 'IFS=":",using "$var"(var=$@)' c=0 for i in "$var" do echo "$((c+=1)):[$i]" done echo "----------" echo 'IFS=":",using $var(var=$@)' c=0 for i in $var do echo "$((c+=1)):[$i]" done echo "----------" var="$@" echo 'IFS=":",using "$var"(var="$@")' c=0 for i in $var do echo "$((c+=1)):[$i]" done echo "----------" echo 'IFS=":",using $var(var="$@")' c=0 for i in "$var" do echo "$((c+=1)):[$i]" done echo "----------" exit 0

    注意:$@和$*中的参数只有在""中才会不同

    Example 9.8 当$IFS为空时的$*和$@

    #!/bin/bash # #如果$IFS被设置为空时,那么"$*"和"$@"将不会像期望的那样echo位置参数 mecho(){ echo "$1,$2,$3" } IFS="" set a b c mecho "$*" mecho $* mecho "$@" mecho $@ #当$IFS设置为空时,$*和$@的行为依赖于正在运行的Bash或者sh的版本,所以在脚本中使用这种"feature"不是明智的行为 exit 0

    其它的特殊参数

    $- 传递给脚本的falg(使用set命令)。参考Example 11.15 注意:这起初是ksh的特征,后来引进到Bash中,但不幸的是,在Bash中它看上去也不能可靠的工作。 使用它的一个可能的方法就是让这个脚本进行自我测试(查看是否是交互的)

    $! 在后台运行的最后一个PID

    #!/bin/bash #lastpid.sh LOG=$0.log TIMEOUT=5 COMMADN1="sleep 100" echo "Logging PIDs background commands for script:$0" >> "$LOG" echo >> "$LOG" echo -n "PID of \"$COMMADN1\": " >> "$LOG" ${COMMAND1} & echo $! >> "$LOG" possibly_hanging_job & { sleep ${TIMEOUT}; eval 'kill -9 $!' &> /dev/null; } #(possibly_hanging_job)制造一个出错的程序 #强制结束一个错误的程序

    $_ 保存之前执行的命令的最后一个参数

    Example 9.9 下划线变量

    #!/bin/bash echo $_ #/bin/bash #只是调用/bin/bash来运行这个脚本 du > /dev/null echo $_ #du ls -al > /dev/null echo $_ #-al(最后的参数) : echo $_ #:

    $? 命令,函数或脚本本身的退出状态(见Example 23.7)

    $$ 脚本自身进程的ID,这个变量经常用来构造一个"unique"的临时文件名 (参考Example A.13,Example 29.6,Example 12.28,Example 11.25)

    9.2 操作字符串

    Bash支持超多的字符串操作,操作的种类和数量令人惊异。但不幸的是,这些工具缺乏集中性。一些是参数替换的子集,但另一些则属于UNIX的expr命令。这就导致了命令语法的不一致和功能的重叠,当然也会引起混乱

    字符串长度

    ${#string} expr length $string expr "$string" : '.*' stringZ=abcABC123ABCabc echo ${#stringZ} #15 echo `expr length $stringZ` #15 echo `expr "$stringZ" : '.*'` #15

    Example 9.10 在一个文本文件的段间插入空行

    #!/bin/bash #paragraph-space.sh #在一个不空的文本文件的段间插入空行 #用法:$0 < FILENAME MINLEN=45 #假定行的长度小于$MINLEN指定的长度,$MINLEN中的值用来描述多少个字符结束一个段 while read line do echo "$line" len=${#line} if [ "$len" -lt "$MINLEN" ];then echo #在短行后边添加一个空行 fi done exit 0

    从字符串的开始位置匹配字符串长度 expr match “$string” ‘$substring’ #$substring是一个正则表达式 expr “$string” : ‘$substring’ #$substring是一个正则表达式

    stringZ=abcABC123ABCabc echo `expr match "$stringZ" 'abc[A-Z]*.2'` #8(abcABC12) echo `expr "$stringZ" : 'abc[A-Z]*.2'` #8(abcABC12)

    索引

    expr index $string $substring #匹配到子串的第一个字符的位置

    stringZ=abcABC123ABCabc echo `expr index "$stringZ" C12` #6(匹配的是C的位置) echo `expr index "$stringZ" 1c` #3(匹配的是c的位置)

    提取子串

    ${string:position} #在string中从位置$position开始提取子串。 如果$string为*或@,那么将提取从位置$position开始的位置参数

    ${string:position:length} #在string中从位置$position开始提取$length长度的子串

    stringZ=abcABC123ABCabc echo ${stringZ:0} #abcABC123ABCabc echo ${stringZ:1} #bcABC123ABCabc echo ${stringZ:7} #23ABCabc echo ${stringZ:7:7} #23A

    反向提取子串

    stringZ=abcABC123ABCabc echo ${stringZ:(-4)} #Cabc echo ${stringZ: -4} #Cabc #注意:不加空格或(),默认提取完整的字符串 echo ${stringZ:-4} #abcABC123ABCabc

    如果$string为*或@,那么将最大的提取从位置$position开始的$length个位置参数

    echo ${*:2} #输出第2个和后面所有的位置参数 echo ${@:2} #与上同义 echo ${*:2:3} #从第2个开始,输出后边3个位置参数

    expr suber $string $position $length #在string中从位置$position开始提取$length长度的子串

    stringZ=abcABC123ABCabc echo `expr suber $stringZ 1 2` #ab echo `expr suber $stringZ 4 3` #ABC

    expr match “$string” '($substring)' #从$string的开始位置提取$substring,$substring是一个正则表达式

    expr “$string” : '($substring)' #从$string的开始位置提取$substring, $substring是一个正则表达式

    stringZ=abcABC123ABCabc echo `expr "$stringZ" : '\(.......\)'` #abcABC1 echo `expr match "$stringZ" '\(.[b-c]*[A-Z]..[0-9]\)'` #abcABC1 echo `expr "$stringZ" : '\(.[b-c]*[A-Z]..[0-9]\)'` #abcABC1

    示例:(便于理解这里的正则)

    echo `expr match "$stringZ" '\(.[b-c]*\)'` #abc echo `expr match "$stringZ" '\(.[b-c]*[A-Z]\)'` #abcA;[A-Z]表示:一个字符的匹配正则 echo `expr match "$stringZ" '\(.[b-c]*[A-Z].\)'` #abcAB,在上边的基础上加一个任意字符 echo `expr match "$stringZ" '\(.[b-c]*[A-Z]..\)'` #abcABC echo `expr match "$stringZ" '\(.[b-c]*[A-Z]..[0-9]\)'` #abcABC1

    子串消除

    ${string#substring} #从$string的左边截掉第一个匹配的$substring

    ${string##substring} #从$string的左边截掉最后一个匹配的$substring

    stringZ=abcABC123ABCabc echo ${stringZ#a*C} #123ABCabc #截掉a和C之间最近的匹配 echo ${stringZ##a*C} #abc #截掉a和C之间最远的匹配

    ${string%substring} #从$string的右边截掉第一个匹配的$substring

    ${string%%substring} #从$string的右边截掉最后一个匹配的$substring

    stringZ=abcABC123ABCabc echo ${stringZ%b*c} #abcABC123ABCa #从$stringZ的后边开始截掉b和c之间的最近匹配 echo ${stringZ%%b*c} #a #从$stringZ的后边开始截掉b和c之间的最远匹配

    Example 9.11 利用修改文件名,来转换图片格式

    #!/bin/bash #cvt.sh #使用来自netpbm包的macptopbm程序,这个程序主要是由Brian Henderson来维护的 #netpbm是大多数Linux发行版的标准部分 OPERATION=macptopbm SUFFIX=pbm #新的文件名后缀 if [ -n "$1" ];then directory=$1 #如果目录名作为第1个参数给出 else directory=$PWD #否则使用当前目录 fi #假设在目标目录中的所有文件都是MacPaint格式的图片文件,以.mac为文件名后缀 for file in $directory/* do filename=${file%.*c} #去掉.mac后缀 $OPERATION $file > "$filename.$SUFFIX" #转换为新文件名 rm -f $file #转换完成后删除原有文件 echo "$filename.$SUFFIX" #从stdout输出 done exit 0

    Example 9.12 模仿getopt命令

    #!/bin/bash #getopt-simple.sh #用法:getopt-simple.sh /values=11 /values1=22 getopt_simple(){ echo "getopt_simple()" echo "参数是 '$*'" until [ -z "$1" ] do echo "加工参数: '$1'" if [ ${1:0:1} = '/' ];then #判断开头是否为'/' tmp=${1:1} #去掉'/' parameter=${tmp%%=*} #从右往左,去掉最后一个=后面的所有值(获取第一个=前面的值) value=${tmp##*=} #从左往右,去掉最后一个=前面的所有值(获取最后一个=后面的值) echo "参数:'$parameter',值:'$value'" eval $parameter=$value fi shift done } getopt_simple $* #传递所有参数到getopt_simple() exit 0

    子串替换

    ${string/substring/replacement} #使用$replacement来替换第一个匹配的$substring

    ${string//substring/replacement} #使用$replacement来替换所有匹配的$substring

    stringZ=abcABC123ABCabc echo ${stringZ/abc/xyz} #xyzABC123ABCabc echo ${stringZ//zbc/xyz} #xyzABC123ABCxyz

    ${string/#substrion/replacement} #如果$substring匹配$string的开头部分,那么就用$replacement来替换$substring

    ${string/%substring/replacement} #如果$substring匹配$string的结尾部分,那么就用$replacement来替换$substring

    stringZ=abcABC123ABCabc echo ${stringZ/#abc/XYZ} #XYZABC123ABCabc echo ${stringZ/%abc/XYZ} #abcABC123ABCXYZ

    9.2.1 使用awk来操作字符串

    Example 9.13 提取字符串的一种可选方法

    #!/bin/bash #substring-extraction.sh String=23skidoo1 #注意:对于awk和Bash来说,它们使用的是不同的string索引系统: #Bash的第一个字符是从0开始记录的 #Awk的第一个字符是从1开始记录的 echo ${String:2:4} #skid #awk中等价于${string:pos:length}的命令是substr(string,pos,length) echo |awk '{print substr("'"${String}"'",3,4)}' #skid #使用一个空的echo通过管道给awk一个假的输入,这样可以不用提供一个文件名 exit 0

    9.2.2 更深的讨论

    关于在脚本中使用字符串更深的讨论,请参考9.3节,h和expr命令列表的相关章节。 关于脚本的例子,见: Example 12-9 Example 9-16 Example 9-17 Example 9-18 Example 9-20

    9.3 参数替换

    操作和扩展变量

    ${parameter} #与$parameter相同,就是parameter的值在特定的上下文中,只有少部分会产生${parameter}的混淆。可以组合起来一起赋值给字符串变量

    your_id=${USER}-on-${HOSTNAME} echo "$your_id" echo "Old \$PATH = $PATH" PATH=${PATH}:/opt/bin echo "New \$PATH = $PATH"

    ${parameter-default},${parameter:-default} #如果parameter没有被set,那么就使用default

    echo ${username-`whoami`}

    注意:${parameter-default}和${parameter:-default}大部分时候是相同的。额外的":"在parameter被声明的时候(而且被赋空值),会有一些不同

    #!/bin/bash #param-sub.sh #一个变量是否被声明,将影响默认选项的触发,甚至于这个变量被设为空 username0= echo "username0 已声明,但设置为null" echo "username0 = ${username0-`whoami`}" #username0 = echo echo "username1 未声明" echo "username1 = ${username-`whoami`}" #username1 = root echo username2= echo "username2 已声明,但设置为null" echo "username2 = ${username2:-`whoami`}" #username0 = root #再来一个 variable= echo "${variable-0}" #空 echo "${variable:-1}" #1 unset variable echo "${variable-2}" #2 echo "${variable:-3}" #3 exit 0 #如果脚本中并没有传入命令行参数,那么default parameter将被使用 DEFAULT_FILENAME=generic.data filename=\${1:-$DEFAULT_FILENAME}

    另外参见Example 3-4,Example 28-2,和 Example A-6。与"使用一个与列表来支持一个默认的命令行参数"的方法相比较。

    ${parameter=default},${parameter:=default} #如果parameter没有被set,那么就设置为default 这两种办法绝大多数时候用法都一样,只有在$parameter被声明并设置为空的时候,才会有区别,和上边的行为一样

    echo ${username=`whoami`}

    ${parameter+alt_value},${parameter:+alt_value} #如果parameter被set,那么就使用alt_value,否则就是用null字符串 这两种办法绝大多数时候用法都一样,只有在$parameter被声明并设置为空的时候,才会有区别,如下:

    a=${param1+xyz} #a= param2= a=${param2+xyz} #a=xyz param3=123 a=${param3+xyz} #a=xyz a=${param4:+xyz} #a= param5= a=${param5:+xyz} #a= param6=123 a=${param6:+xyz} #a=xyz

    ${parameter?err_msg},${parameter:?err_msg} #如果parameter被set,那么就使用set值,否则就print err_msg 这两种办法绝大多数时候用法都一样,只有在$parameter被声明并设置为空的时候,才会有区别,和上边的行为一样

    Example 9.14 使用参数替换和errot messages

    #!/bin/bash # #检查一些系统的环境变量。这是个好习惯 #比如,如果$USER(在console上的用户名)没有被set,那么系统就不会认你。 #${variablename?}结果可以用来在一个脚本中检查变量是否被set : ${HOSTNAME?}${USER?}${HOME?}${MAIL?} &> /dev/null echo echo "Name of the machine is $HOSTNAME" echo "You are $USER" echo "Your home directory is $HOME" echo "Your main INBOX is located in $MAIL" echo echo "If you are reading this message" echo "critical environmental variables have been set" echo ThisVariable=Value-of-ThisVariable : ${ThisVariable?} &> /dev/null echo "Value of ThisVariable is $ThisVariable" echo : ${ZZXy23AB?"ZZXy23AB has not been set"} &> /dev/null #如果ZZXy23AB没有被set,那么这个脚本将会在这里终止,并提示如下错误 #$0: line 23: ZZXy23AB: ZZXy23AB has not been set #你可以指定错误消息 #: ${variablename?"ERROR MESSAGE"} #同样的结果 #dummy_variable=${ZZXy23AB?} #dummy_variable=${ZZXy23AB?"ZZXy23AB has not been set"} #同set -u命令来比较这些检查变量是否被set的方法 echo "You will not see this message,because script already terminated" HERE=0 exit $HERE #事实上这个脚本将返回值1作为退出状态

    Example 9.15 参数替换和"usage"messages

    #!/bin/bash #usage-message.sh : ${1?"Usage:$0 ARGUMENT"} #如果没有命令行参数,那么脚本将在此处退出。并且打出如下错误消息 #usage-message.sh:1:Usage:usage-message.sh ARGUMENT echo "These two lines echo only if command-line parameter given" echo "command line parameter = \"$1\"" exit 0 #如果有命令行参数,将在此退出

    参数替换和扩展

    下边的表达式是使用expr字符串匹配操作的补充(见Example 12.9) 这些特定的使用方法绝大多数情况下都是用来分析文件目录名

    变量长度/子串删除

    ${#var} #字符串长度($var的字符串数量)。 对于一个数组,${#array}是数组中第一个元素的长度。

    一些例外: ${#}和${#@}将给出位置参数的个数 对于数组来说${#array[]}和${$#array[@]}将给出数组元素的个数

    Example 9.16 变量长度

    #!/bin/bash #length.sh E_NO_ARGS=65 if [ $# -eq 0 ];then echo "请使用一个或多个命令行参数调用此脚本" exit $E_NO_ARGS fi var01=abcdEFGH28ij echo "var01 = ${var01}" #var01 = abcdEFGH28ij echo "Length of var01 = ${#var01}" #Length of var01 = 12 #现在,让我们试试在里面嵌入一个空格 var02="abcd EFGH28ij" echo "var02 = ${var02}" #var02 = abcd EFGH28ij echo "Length of var02 = ${#var02}" #Length of var02 = 13 echo "传递给脚本的命令行参数数 = ${#@}" echo "传递给脚本的命令行参数数 = ${#*}" exit 0

    ${var#Pattern},${var##Pattern} #从$var开头删除最近或最远匹配$Pattern的子串

    来自Example A.7例子的一部分

    #来自"days-between.sh"例子的一个函数 #去掉传递进来的参数开头的0 strip_leading_zero (){ return=${1#0} }

    下边是曼弗雷德·施瓦布的对上边函数的一个改版

    strip_leading_zero2(){ #去掉开头的0,因为如果不去的话,Bash将会把这个值作为8进制解释 shopt -s extglob #打开扩展globbing local val=${1##=+(0)} #使用局部变量,匹配最长的连续的0 shopt -u extglob #关闭扩展globbing _strip_leading_zero2=${val:-0} #如果输入0,那么返回0来代替"" }

    shopt命令

    shopt命令是set命令的一种替代,很多方面都和set命令一样,但它增加了很多选项。 可以使用

    -p选项来查看shopt选项的设置 -u表示关闭一个选项 -s表示开启一个选项

    以下是shopt命令的选择介绍:

    cdable_vars 如果给cd内置命令的参数不是一个目录,就假设它是一个变量名,变量的值是将要转换到的目录 cdspell 纠正cd命令中目录名的较小拼写错误.检查的错误包括颠倒顺序的字符,遗漏的字符以及重复的字符.如果找到一处需修改之处,正确的路径将打印出,命令将继续.只用于交互式shell checkhash bash在试图执行一个命令前,先在哈希表中寻找,以确定命令是否存在.如果命令不存在,就执行正常的路径搜索 checkwinsize bash在每个命令后检查窗口大小,如果有必要,就更新LINES和COLUMNS的值 cmdhist bash试图将一个多行命令的所有行保存在同一个历史项中.这是的多行命令的重新编辑更方便 dotglob Bash在文件名扩展的结果中包括以点(.)开头的文件名 execfail 如果一个非交互式shell不能执行指定给exec内置命令作为参数的文件,它不会退出.如果exec失败,一个交互式shell不会退出 expand_aliases 别名被扩展.缺省为打开 extglob 打开扩展的模式匹配特性(正常的表达式元字符来自Korn shell的文件名扩展) histappend 如果readline正被使用,用户有机会重新编辑一个失败的历史替换 histverify 如果设置,且readline正被使用,历史替换的结果不会立即传递给shell解释器.而是将结果行装入readline编辑缓冲区中,允许进一步修改 hostcomplete 如果设置,且readline正被使用,当正在完成一个包含@的词时bash将试图执行主机名补全.缺省为打开 huponexit 如果设置,当一个交互式登录shell退出时,bash将发送一个SIGHUP(挂起信号)给所有的作业 interactive_comments 在一个交互式shell中.允许以#开头的词以及同一行中其他的字符被忽略.缺省为打开 lithist 如果打开,且cmdhist选项也打开,多行命令讲用嵌入的换行符保存到历史中,而无需在可能的地方用分号来分隔 mailwarn 如果设置,且bash用来检查邮件的文件自从上次检查后已经被访问,将显示消息”The mail in mailfile has been read” nocaseglob 如果设置,当执行文件名扩展时,bash在不区分大小写的方式下匹配文件名 nullglob 如果设置,bash允许没有匹配任何文件的文件名模式扩展成一个空串,而不是他们本身 promptvars 如果设置,提示串在被扩展后再进行变量和参量扩展.缺省为打开 restricted_shell 如果shell在受限模式下启动就设置这个选项.该值不能被改变.当执行启动文件时不能复位该选项,允许启动文件发现shell是否受限 shift_verbose 如果该选项设置,当移动计数超出位置参量个数时,shift内置命令将打印一个错误消息 sourcepath 如果设置,source内置命令使用PATH的值来寻找作为参数提供的文件的目录.缺省为打开 source(.)的同义词

    实例: 查看extglob选项是否开启(默认是off)

    [root@master load_data]# shopt extglob extglob off

    开启 extglob 选项:

    shopt -s extglob

    开启 extglob 选项:

    shopt -u extglob

    开启之后,以下5个模式匹配操作符将被识别:

    ?(pattern-list) - 所给模式匹配0次或1次; *(pattern-list) - 所给模式匹配0次以上包括0次; +(pattern-list) - 所给模式匹配1次以上包括1次; @(pattern-list) - 所给模式仅仅匹配1次; !(pattern-list) - 不匹配括号内的所给模式。

    实例: 删除文件名不以jpg结尾的文件:

    rm -rf !(*jpg)

    删除文件名以jpg或png结尾的文件:

    rm -rf *@(jpg|png)

    另一个例子

    echo `basename $PWD` #当前工作目录的basename echo "${PWD##*/}" #当前工作目录的basename echo echo `basename $0` #脚本名字 echo $0 #脚本名字 echo "${0##*/}" #脚本名字 echo filename=test.data echo "${filename##*.}" #data

    ${var%Pattern},${var%%Pattern} #从$var结尾删除最近或最远匹配$Pattern的子串

    Example 9.17 参数替换中的模式匹配

    #!/bin/bash #patt-matching.sh #使用# ## % %%来进行参数替换操作的模式匹配 var1=abcd12345abc6789 pattern1=a*c echo echo "var1 = $var1" echo "var1 = ${var1}" echo "Number of characters in ${var1} = ${#var1}" echo echo "pattern1 = $pattern1" echo "----------" echo '${var1#$pattern1} ='"${var1#$pattern1}" #d12345abc6789 #最短的匹配,去掉$var的前3个字符 echo '${var1##$pattern1} ='"${var1##$pattern1}" #6789 #最远的匹配,去掉$var的前12个字符 echo;echo pattren2=b*9 echo "var1 = $var1" echo echo "pattern2 = $pattern2" echo "----------" echo '${var%pattern2} ='"${var%pattern2}" #abcd12345a echo '${var%%pattern2} ='"${var%%pattern2}" #a #记住:#和## 从字符串的左边开始,并且去掉左边的字符串 # %和%% 从字符串的右边开始,并且去掉右边的字符串 echo exit 0

    Example 9.18 重命名文件扩展名

    #!/bin/bash #rfe.sh #用法:rfs old_extension new_extension #例子: #将制定目录的所有*.gif文件都重命名为*.jpg #用法:rfs gif jpg E_BADARGS=65 case $# in 0|1) echo "Usage:`basename $0` old_file_suffix new_file_suffix" exit $E_BADARGS ;; esac for filename in *.$1 do mv $filename ${filename%$1}$2 #从筛选出的文件中先去掉以第一个参数结尾的扩展名, #然后作为扩展名把第2个参数添加上 done exit 0

    变量扩展/子串替换

    这些结构都是从ksh中吸收来的 ${var:pos} #变量var从位置pos开始扩展

    ${var:pos:len} #从位置pos开始,并扩展len长度个字符。见Example A.14(这个例子里有这种操作的一个创造性用法)

    ${var/Pattern/Replacement} #使用Replacement来替换var中的第一个Pattern的匹配

    ${var//Pattern/Replacement} #全局替换。在var中所有的匹配,都会用Replacement来替换

    如果Replacement被忽略的话,那么所有匹配到的Pattern都会被删除

    Example 9.19 使用模式匹配来分析比较特殊的字符串

    #!/bin/bash # var1=abcd-1234-defg echo "var1 = $var1" t=${var1#*-*} #1234-defg echo "var1 (所有,直至并包括第一次剥离) = $t" #t=${var1#*-} 在这个例子中作用是一样的,因为#匹配这个最近的字符串,并且*匹配前边的任何字符串,包括一个空字符 t=${var1##*-*} #空 echo "如果var1包含\"-\",则返回空字符串... var1 = $t" echo t=${var1%*-*} #abcd-1234 echo "var1 (所有的东西都被剥离) = $t" echo path_name=/home/bozo/ideas/thoughts.for.today echo "path_name = $path_name" t=${path_name##/*/} #thoughts.for.today echo "path_name,去掉前缀 = $t" #在这个特定的例子中,与t=`basename $path_name`的作用一致 #t=${path_name%/};t=${t##*/} 是一个更一般的解决办法,但有时还是不行 #如果$path_name以一个新行结束,那么`basename $path_name`将不能工作,但是上边这个表达式可以 t=${path_name%/*.*} #/home/bozo/ideas #与t=`dirname $path_name`效果相同 echo "path_name,去掉后缀 = $t" #在某些情况下将失效,比如:"../","/foo",#"foo/","/" #删除后缀,尤其是在basename没有后缀的时候,但是dirname还是会使问题复杂化 echo t=${path_name:11} #ideas/thoughts.for.today echo "$path_name,去掉前11个字符 = $t" t=${path_name:11:5} #ideas echo "$path_name,去掉前11个字符,长度5 = $t" echo t=${path_name/bozo/clown} #/home/clown/ideas/thoughts.for.today echo "$path_name with \"bozo\" 替换为 \"clown\" = $t" t=${path_name/today/} #/home/bozo/ideas/thoughts.for. echo "$path_name with \"tiday\" deleted = $t" t=${path_name//o/O} #/hOme/bOzO/ideas/thOughts.fOr.tOday echo "$path_name with all o's 大写 = $t" t=${path_name//o/} #/hme/bz/ideas/thughts.fr.tday echo "$path_name with all o's deleted = $t" exit 0

    ${var/#Pattern/Replacement} #如果var的前缀匹配到了Pattern,那么就用Replacement来替换Pattern

    ${var/%Pattern/Replacement} #如果var的后缀匹配到了Pattern,那么就用Replacement来替换Pattern

    Example 9.20 对字符串的前缀或后缀使用匹配模式

    #!/bin/bash #var-match.sh #对字符串的前后缀使用匹配替换的一个样本 v0=abc1234zip1234abc echo "v0 = $v0" echo #匹配字符串的前缀 v1=${v0/#abc/ABCDEF} echo "v1 = $v1" #v1 = ABCDEF1234zip1234abc #匹配字符串的后缀 v2=${v0/%abc/ABCDEF} echo "v2 = $v2" #v2 = abc1234zip1234ABCDEF echo # ---------------------- # 必须在开头或者结尾匹配, # 否则将不会产生替换结果 # ---------------------- v3=${v0/#123/000} echo "v3 = $v3" #v3 = abc1234zip1234abc v4=${v0/%123/000} echo "v4 = $v4" #v4 = abc1234zip1234abc exit 0

    ${!varprefix},${!varprefix@}* #使用变量的前缀来匹配前边所有声明过的变量

    xyz23=whatever xyz24= a=${!xyz*} #以xyz作为前缀,匹配所有前边声明过的变量 echo "a = $a" #a = xyz23 xyz24 a=${!xyz@} #同上 echo "a = $a" #同上

    9.4 指定类型的变量:declare或者typeset

    declare或者typeset内建命令(这两个命令是完全一样的)允许指定变量的具体类型。在某些特定的语言中,这是一种指定类型的很弱的形式。declare命令是在Bash版本2或之后的版本才被加入的。typeset命令也可以工作在ksh脚本中

    declare/typeset选项 -r 只读

    declare -r var1

    (declare -r var1与readonly var1是完全一样的),这和C语言中的const关键字一样,都是强制指定只读。如果你尝试修改一个只读变量的值,那么你将得到一个错误信息。

    -i 整型

    declare -i number #这个脚本将把变量number后边的赋值视为一个整型 number=3 echo "number = $number" #number = 3 number=three echo "number = $number" #number = 0

    如果把一个变量指定为整形,那么即使没有expr和let命令,也允许使用特定的算术运算

    n=6/3 echo "n = $n" #n = 6/3 declare -i n n=6/3 echo "n = $n" #n = 2

    -a 数组

    declare -a indices

    变量indices将被视为数组

    -f 函数

    declare -f

    如果使用declare -f而不带参数的话,将会列出这个脚本中之前定义的所有函数

    declare -f function_name

    如果使用declare -f function_name这种形式的话,将会只列出这个函数的名字。

    -x export

    declare -x var3

    这种使用方式,将会把var3 export出来

    Example 9.21 使用declare来指定变量的类型

    #!/bin/bash # func1 (){ echo This is a function } declare -f #列出之前所有的函数 echo declare -i var1 #var1是个整型 var1=1234 echo "var1 declared as $var1" var1=var1+1 #变量声明不需要使用let echo "var1 incremented by 1 is $var1" #尝试将变量修改为整形 echo "试图将var1更改为浮点值" var1=123.4 #结果将是一个错误消息,并且变量并没有被修改 echo "var1 is still $var1" echo declare -r var2=12.34 #declare允许设置变量的属性,并且同时分配变量的值 echo "var2 declared as $var2" #尝试修改只读变量 var2=56.78 #产生一个错误消息,并且从脚本退出 echo "var2 is still $var2" #这行将不会被执行 exit 0 #脚本将不会从此退出

    注意:使用declare内建命令将会限制变量的作用域

    foo (){ F00="bar" } bar(){ foo echo $FOO } bar #输出bar

    然而。。。

    foo (){ declare FOO="bar" } bar(){ foo echo $FOO } bar #输出空

    9.5 变量的间接引用

    假设一个变量的值是另一个变量的名字。我们有可能从第1个变量中取得第2个变量的值吗? 比如:如果a=letter_of_alphabet接着letter_of_alphabet=z,那么我们能从a中得到z吗? 答案是:当然可以,并且这被称为间接引用。它使用一个不常用的符号

    eval var1=\\$$var2

    Example 9.22 间接引用

    #!/bin/bash #ind-ref.sh: 间接变量引用 #存取一个变量的值的值 a=letter_of_alphabet letter_of_alphabet=z echo #直接引用 echo "a = $a" #a = letter_of_alphabet #间接引用 eval a=\$$a echo "Now a = $a" #Now a = z echo #现在让我们试试修改di2个引用的值 t=table_cell_3 table_cell_3=24 echo "\"table_cell_3\" = $table_cell_3" #"table_cell_3" = 24 echo -n "dereferenced \"t\" = ";eval echo \$$t #dereferenced "t" = 24 echo t=table_cell_3 NEW_VAL=123 table_cell_3=$NEW_VAL echo "Changing value of \"table_cell_3\" to $NEW_VAL" #Changing value of "table_cell_3" to 123 echo "\"table_cell_3\" now $table_cell_3" #"table_cell_3" now 123 echo -n "dereferenced \"t\" now ";eval echo \$$t #dereferenced "t" now 123 #eval将获得两个参数echo和\$$t(与$table_cell_3等价) echo exit 0

    间接引用到底有什么应用价值?他给Bash添加了一种类似C语言指针的功能,在Example 34.3中有例子。并且还有一些其它的有趣的应用

    尼尔斯·拉特克展示了如何建立一个动态变量名字并且取出其中的值。当sourcing(包含)配置文件时,这很有用

    #!/bin/bash # #--------------------------- #这部分内容可能来自于单独的文件. isdnMyProviderRemoteNet=172.16.0.100 isdnYourProviderRemoteNet=10.0.0.10 isdnOnlineService="MyProvider" #--------------------------- remoteNet=$(eval "echo \$$(echo isdn${isdnOnlineService}RemoteNet)") echo $remoteNet #172.16.0.100 remoteNet=$(eval "echo \$$(echo isdnMyProviderRemoteNet)") echo $remoteNet #172.16.0.100 remoteNet=$(eval "echo \$isdnMyProviderRemoteNet") echo $remoteNet #172.16.0.100 remoteNet=$(eval "echo $isdnMyProviderRemoteNet") echo $remoteNet #172.16.0.100

    Example 9.23 传递一个间接引用给awk

    #!/bin/bash # ARGS=2 E_WRONGARGS=65 if [ $# -ne "$ARGS" ];then echo "Usage: `basename $0` filename number" exit $E_WRONGARGS fi filename=$1 number=$2 #-----awk脚本开始 awk " { total += \$${number} } END { print total } " "$filename" #-----awk脚本结束 exit 0 #经测试,此awk的命令是将$filename文件中每行中$number列的数字累加。

    注意:Bash并不支持指针的算术运算,并且这严格的限制了间接引用的使用。事实上,在脚本语言中,间接引用本来就是丑陋的部分

    9.6 $RANDOM 产生随机数

    $RANDOM是Bash的内部函数(并不是常量),这个函数将返回一个范围在0-32767之间的一个伪随机数。它不应该被用来产生秘钥

    Example 9.24 产生随机数

    #!/bin/bash # #$RANDOM在每次调用的时候,返回一个不同的随机数 #指定的范围是:0-32767(有符号的16-bit整数) MAXCOUNT=10 count=1 echo echo "$MAXCOUNT random numbers:" echo "----------" while [ "$count" -le $MAXCOUNT ] do number=$RANDOM echo $number let "count += 1" done echo "----------" #如果你需要在一个特定范围内产生一个随机数int,那么使用modulo(模)操作。 #这将返回一个除法操作的余数 RANGE=500 echo number=$RANDOM let "number %= $RANGE" echo "Random number less than $RANGE --- $number" echo #如果你需要产生一个比你指定的最小边界大的随机数 #那么建立一个test循环,来丢弃所有产生对比这个数小的随机数 FLOOR=200 number=0 while [ "$number" -le $FLOOR ] do number=$RANDOM done echo "Random number greater than $FLOOR --- $number" echo #结合上边两个例子的技术,来达到获得在指定的上下线之间来产生随机数 number=0 while [ "$number" -le $FLOOR ] do number=$RANDOM let "number %= $RANGE" done echo "Random number between $FLOOR and $RANGE --- $number" echo #产生一个二元选择,就是true和false两个值 BINARY=2 T=1 number=$RANDOM let "number %= $BINARY" if [ "$number" -eq $T ];then echo "true" else echo "false" fi echo #掷骰子 SPOTS=6 die1=0 die2=0 #是否让SPOTS=7?比加1更好呢? #答案是:不能让SPOTS=7。因为这样的话会有0,骰子没有0 let "die1 = $RANDOM % $SPOTS +1" let "die2 = $RANDOM % $SPOTS +1" #上边的算术操作是先取余,在加1 let "throw = $die1 + $die2" echo "Throw of the dice = $throw" echo exit 0

    Example 9.25 从一副扑克牌中取出一张随机牌

    #!/bin/bash #pic-card.sh #这是一个从数组中取出随机元素的一个例子 Suites="梅花 方块 红桃 黑桃" Denominations="2 3 4 5 6 7 8 9 10 J Q K A" #注意变量的多行展开 suite=($Suites) #读到数组变量中 denomination=($Denominations) num_suites=${#suite[*]} #计算有多少个元素 num_denominations=${#denomination[*]} echo -n "${suite[$((RANDOM%num_suites))]}" echo "${denomination[$((RANDOM%num_denominations))]}" exit 0

    Jipe展示了一系列的在一定范围中产生随机数的方法

    #在6到30之间产生随机数 number=$(RANDOM%25+6) #还是产生6-30之间的随机数,但是这个数字必须被3整除 number=$(((RANDOM%30/3+1)*3)) #$RANDOM%30=0-29;/3取商=0-9;+1=1-10;*3肯定能被3整除 #注意:这可能不会在所有时候都能正常地运行。 #Frank Wang建议用下边的方法来取代 number=$((RANDOM%27/3*3+6))

    比尔·格拉德沃提出了一个重要的规则来产生正数(经测试,貌似有问题)

    rnumber=$(((RANDOM%(max-min+divisibleBy))/divisibleBy*divisibleBy+min))

    这里Bill给出了一个通用函数,这个函数返回一个在两个指定值之间的随机数

    Example 9.26 两个指定值之间的随机数(这个脚本后边没看懂,放弃了)

    #!/bin/bash #random-between.sh randomBetween(){ #在$max和$min之间产生一个正或负的随机数,并且可被$divisibleBy整除 syntax(){ #在函数中内嵌函数 echo echo "Syntax:randomBetween [min] [max] [multiple]" echo echo "最多需要3个参数,但所有参数都是完全可选的" echo "min 是最小值" echo "max 是最大值" echo "multiple 指定的答案必须是此值的倍数" echo echo "如果缺少任何值,则提供的默认区域为:0 32767 1" echo "成功完成返回0,不成功完成返回函数语法和1" echo "答案在全局变量randomBetweeAnswer中返回" echo "正确处理任何传递参数的负值" } #分配默认值,用来处理没有参数传递进来的时候 local min=${1:-0} local max=${2:-32767} local divisibleBy=${3:-1} local x local spread #确认divisibleBy是正值 [ ${divisibleBy} -lt 0 ] && divisibleBy=$((0-divisibleBy)) #完整性检查 # if [ $# -gt 3 -o ${dicisibleBy} -eq 0 -o ${min} -eq ${max} ];then # syntax # return 1 # fi #上边的完整性检查(报错:参数太多) if [ $# -gt 3 ];then if [ ${min} -eq ${max} ];then if [ ${dicisibleBy} -eq 0 ];then syntax return 1 fi fi fi #观察是否min和max颠倒了 if [ ${min} -gt ${max} ];then #交换它们 x=${min} min=${max} max=${x} fi #如果min自己并不能够被$divisibleBy整除,那么就调整min的值, #+ 使其能够被$divisibleBy整除,前提是不能放大范围。 if [ $((min/divisibleBy*divisibleBy)) -ne ${min} ];then if [ ${min} -lt 0 ];then min=$((min/divisibleBy*divisibleBy)) else min=$((((min/divisibleBy)+1)*divisibleBy)) fi fi #如果max自己并不能够被$divisibleBy整除,那么就调整max的值, #+ 使其能够被$divisibleBy整除,前提是不能放大范围。 if [ $((min/divisibleBy*divisibleBy)) -ne ${min} ];then if [ ${min} -lt 0 ];then min=$((min/divisibleBy*divisibleBy)) else min=$((((min/divisibleBy)+1)*divisibleBy)) fi fi #-------------------------------------- #现在,来做真正的工作 #注意,为了得到对于断电来说合适的分配,随机值得范围不得不在0和abs(max-min)+divisibleBy之间,而不是abs(max-min)+1 #对于端点来说,这个少了的增加将会产生合适的分配 #修改这个公式,使用abs(max-min)+1来代替abs(max-min)+divisibleBy的话, #+ 也能够产生正确的答案,但是在这种情况下生成的随机值对于正好为端点倍数的 #+ 这种情况来说并不完美,因为在正好为端点倍数的情况下的随机率比较低, #+ 因为你在+1而已,这比正常地公式所产生的的机率要小的多(正常为加divisibleBy) #-------------------------------------- spread=$((max-min)) [ ${spread} -lt 0 ] && spread=$((0-spread)) let spread+=divisibleBy randomBetweenAnswer=$(((RANDOM%spread)/divisibleBy*divisibleBy+min)) return 0 #然而保罗·马塞尔·科埃略·阿拉高指出,当$max和$min不能被$divisibleBy整除时 #+ 这个公式将会失败 #他建议使用如下公式: #+ rnumber=$(((RANDOM%(max-min+1)+min)/divisibleBy*divisibleBy)) } #让我们来测试一下这个函数 min=-14 max=20 divisibleBy=3 #产生一个数组answers,answers的下标用来表示在范围内可能出现的值, #+ 而内容记录的是对于这个值出现的次数,如果我们循环自购多次, #+ 一定会得到一次出现机会 declare -a answer minimum=${min} maximum=${max} if [ $((minimum/divisibleBy)) -ne ${minimum} ];then if [ ${minimum} -lt 0 ];then minimum=$((minimum/divisibleBy*divisibleBy)) else minimum=$((((minimum/divisibleBy)+1)*divisibleBy)) fi fi #如果maximum自己并不能够被$divisibleBy整除, #+ 那么就调整 maximum 的值,使其能够被$divisibleBy 整除,前提是不能放大范围. if [ $((maximum/divisibleBy*divisibleBy)) -ne ${maximum} ]; then if [ ${maximum} -lt 0 ]; then maximum=$((((maximum/divisibleBy)-1)*divisibleBy)) else maximum=$((maximum/divisibleBy*divisibleBy)) fi fi # 我们需要产生一个下标全为正的数组, #+ 所以我们需要一个 displacement 来保正都为正的结果. displacement=$((0-minimum)) for ((i=${minimum}; i<=${maximum}; i+=divisibleBy)); do answer[i+displacement]=0 done # 现在我们循环足够多的次数来得到我们想要的答案. loopIt=1000 # 脚本作者建议 100000,但是这实在是需要太长的时间了. for ((i=0; i<${loopIt}; ++i)); do # 注意,我们在这里调用 randomBetween 函数时,故意将 min 和 max 颠倒顺序 #+ 我们是为了测试在这种情况下,此函数是否还能得到正确的结果. randomBetween ${max} ${min} ${divisibleBy} # 如果答案不是我们所预期的,那么就报告一个错误. [ ${randomBetweenAnswer} -lt ${min} -o ${randomBetweenAnswer} -gt ${max} ] && echo MIN or MAX error - ${randomBetweenAnswer}! [ $((randomBetweenAnswer%${divisibleBy})) -ne 0 ] && echo DIVISIBLE BY error - ${randomBetweenAnswer}! # 将统计值存到 answer 之中. answer[randomBetweenAnswer+displacement]=$((answer[randomBetweenAnswer+displacement]+1)) done # 让我们察看一下结果 for ((i=${minimum}; i<=${maximum}; i+=divisibleBy)); do [ ${answer[i+displacement]} -eq 0 ] && echo "We never got an answer of $i." || echo "${i} occurred ${answer[i+displacement]} times." done exit 0

    $RANDOM到底有多随机?最好的办法就是写个脚本来测试一下。跟踪随机数的分配情况。让我们用随机数摇一个骰子

    Example 9.27 使用随机数来摇一个骰子

    #!/bin/bash RANDOM=$$ #使用脚本的进程ID来作为随机数的产生种子(也可以不设置) PIPS=6 #一个骰子有6面 MAXTHROWS=600 #如果你没别的事干,可以增加这个数值 throw=0 #抛骰子的次数 ones=0 twos=0 threes=0 fours=0 fives=0 sixes=0 print_result(){ echo echo "noes = $ones" echo "twos = $twos" echo "threes = $threes" echo "fours = $fours" echo "fives = $fives" echo "sixes = $sixes" echo } update_count(){ case "$1" in 0) let "ones += 1";; 1) let "twos += 1";; 2) let "threes += 1";; 3) let "fours += 1";; 4) let "fives += 1";; 5) let "sixes += 1";; esac } echo while [ "$throw" -lt "$MAXTHROWS" ] do let "die1 = RANDOM % $PIPS" update_count $die1 let "throw += 1" done print_result exit 0

    一个练习 抛1000次硬币的形式

    #!/bin/bash PIPS=2 #一枚硬币有2面 MAXTHROWS=1000 #总1000次 throw=0 #抛硬币的次数 front=0 back=0 print_result(){ echo echo "front = $front" echo "back = $back" echo } update_count(){ case "$1" in 0) let "front += 1";; 1) let "back += 1";; esac } echo while [ "$throw" -lt "$MAXTHROWS" ] do let "die1 = RANDOM % $PIPS" update_count $die1 let "throw += 1" done print_result exit 0

    像我们在上边的例子中看到的,最好在每次随机数产生时都是用新的种子。因为如果使用同样的种子的话,那么随机数将产生相同的序列。(C中random()函数也会有这样的行为)

    Example 9.28 重新分配随机数种子

    #!/bin/bash #seeding-random.sh MAXCOUNT=25 random_numbers(){ count=0 while [ "$count" -lt "$MAXCOUNT" ];then do number=$RANDOM echo -n "$number" let "count += 1" done } echo;echo RANDOM=1 #为随机数的产生设置RANDOM种子 random_numbers echo;echo RANDOM=1 #设置同样的种子,将会和上边产生的随机数列相同 #+ 复制一个相同的随机数序列在什么时候有用呢? random_numbers echo;echo RANDOM=2 random_numbers echo;echo #一个有想象力的方法。。。 SEED=$(head -1 /dev/urandom | od -N 1 | awk '{print $2}') #首先从/dev/urandom(系统伪随机设备文件)中取出1行, #+ 然后这个可以打印行转换为(8进制)数,通过使用od命令 #+ 最后使用awk来获取一个数,这个数将作为随机数产生的种子 RANDOM=$SEED random_numbers echo;echo exit 0

    随机数种子的作用就是:相同的随机数种子,拿到的随机数是相同的 例:

    [root@localhost aaa]# RANDOM=10;echo $RANDOM 4230 [root@localhost aaa]# RANDOM=10;echo $RANDOM 4230 [root@localhost aaa]# RANDOM=1;echo $RANDOM 16807 [root@localhost aaa]# RANDOM=1;echo $RANDOM 16807

    注意:/dev/urandom 设备文件提供了一种比单独使用$RANDOM更好的,能产生更随机的随机数的方法。 dd if=/dev/urandom of=targetfile bs=1 count=XX能够产生一个很分散的伪随机数。然而,将这个数赋值到一个脚本文件的变量中,还需要可操作性,比如使用od命令(就像上边的例子,见Example 12.13),或者使用dd命令(见Example 12.55),或者管道到md5sum命令中(见Example 33.14)

    od

    od指令会读取所给予的文件的内容,并将其内容以八进制字码呈现出来。 语法

    od [-abcdfhilovx][-A <字码基数>][-j <字符数目>][-N <字符数目>][-s <字符串字符数>][-t <输出格式>][-w <每列字符数>][--help][--version][文件...]

    参数:

    -a  此参数的效果和同时指定"-ta"参数相同。 -A<字码基数>  选择要以何种基数计算字码。 -b  此参数的效果和同时指定"-toC"参数相同。 -c  此参数的效果和同时指定"-tC"参数相同。 -d  此参数的效果和同时指定"-tu2"参数相同。 -f  此参数的效果和同时指定"-tfF"参数相同。 -h  此参数的效果和同时指定"-tx2"参数相同。 -i  此参数的效果和同时指定"-td2"参数相同。 -j<字符数目>或--skip-bytes=<字符数目>  略过设置的字符数目。 -l  此参数的效果和同时指定"-td4"参数相同。 -N<字符数目>或--read-bytes=<字符数目>  到设置的字符数目为止。 -o  此参数的效果和同时指定"-to2"参数相同。 -s<字符串字符数>或--strings=<字符串字符数>  只显示符合指定的字符数目的字符串。 -t<输出格式>或--format=<输出格式>  设置输出格式。 -v或--output-duplicates  输出时不省略重复的数据。 -w<每列字符数>或--width=<每列字符数>  设置每列的最大字符数。 -x  此参数的效果和同时指定"-h"参数相同。 --help  在线帮助。 --version  显示版本信息。

    实例 创建 tmp 文件:

    $ echo abcdef g > tmp $ cat tmp abcdef g

    使用 od 命令:

    $ od -b tmp 0000000 141 142 143 144 145 146 040 147 012 0000011

    使用单字节八进制解释进行输出,注意左侧的默认地址格式为八字节:

    $ od -c tmp 0000000 a b c d e f g \n 0000011

    使用ASCII码进行输出,注意其中包括转义字符

    $ od -t d1 tmp 0000000 97 98 99 100 101 102 32 103 10 0000011

    使用单字节十进制进行解释

    $ od -A d -c tmp 0000000 a b c d e f g \n 0000009

    dd

    dd命令用于读取、转换并输出数据。 dd可从标准输入或文件中读取数据,根据指定的格式来转换数据,再输出到文件、设备或标准输出。 参数说明:

    if=文件名:指定源文件。 of=文件名:指定目的文件。 ibs=bytes:一次读入bytes个字节,即指定一个块大小为bytes个字节。 obs=bytes:一次输出bytes个字节,即指定一个块大小为bytes个字节。 bs=bytes:同时设置读入/输出的块大小为bytes个字节。 cbs=bytes:一次转换bytes个字节,即指定转换缓冲区大小。 skip=blocks:从输入文件开头跳过blocks个块后再开始复制。 seek=blocks:从输出文件开头跳过blocks个块后再开始复制。 count=blocks:仅拷贝blocks个块,块大小等于ibs指定的字节数。 conv=<关键字>,关键字可以有以下11种: conversion:用指定的参数转换文件。 ascii:转换ebcdic为ascii ebcdic:转换ascii为ebcdic ibm:转换ascii为alternate ebcdic block:把每一行转换为长度为cbs,不足部分用空格填充 unblock:使每一行的长度都为cbs,不足部分用空格填充 lcase:把大写字符转换为小写字符 ucase:把小写字符转换为大写字符 swab:交换输入的每对字节 noerror:出错时不停止 notrunc:不截短输出文件 sync:将每个输入块填充到ibs个字节,不足部分用空(NUL)字符补齐。 --help:显示帮助信息 --version:显示版本信息

    实例 在Linux 下制作启动盘,可使用如下命令:

    dd if=boot.img of=/dev/fd0 bs=1440k

    将testfile文件中的所有英文字母转换为大写,然后转成为testfile_1文件,在命令提示符中使用如下命令:

    dd if=testfile_2 of=testfile_1 conv=ucase

    其中testfile_2 的内容为:

    $ cat testfile_2 #testfile_2的内容 HELLO LINUX! Linux is a free unix-type opterating system. This is a linux testfile! Linux test

    转换完成后,testfile_1 的内容如下:

    $ dd if=testfile_2 of=testfile_1 conv=ucase #使用dd 命令,大小写转换记录了0+1 的读入 记录了0+1 的写出 95字节(95 B)已复制,0.000131446 秒,723 KB/s cmd@hdd-desktop:~$ cat testfile_1 #查看转换后的testfile_1文件内容 HELLO LINUX! LINUX IS A FREE UNIX-TYPE OPTERATING SYSTEM. THIS IS A LINUX TESTFILE! LINUX TEST #testfile_2中的所有字符都变成了大写字母

    由标准输入设备读入字符串,并将字符串转换成大写后,再输出到标准输出设备,使用的命令为:

    dd conv=ucase

    输入以上命令后按回车键,输入字符串,再按回车键,按组合键Ctrl+D 退出,出现以下结果:

    $ dd conv=ucase Hello Linux! #输入字符串后按回车键 HELLO LINUX! #按组合键Ctrl+D退出,转换成大写结果 记录了0+1 的读入 记录了0+1 的写出 13字节(13 B)已复制,12.1558 秒,0.0 KB/s

    Example 9.29 使用awk产生伪随机数

    #!/bin/bash #random2.sh :产生一个范围0-1的伪随机数 #使用awk的rand()函数 AWKSCRIPT='{ srand();print rand()}' #Command(s)/传到awk中的参数 #注意:srand()函数用来产生awk的随机数种子。 echo -n "Random number between 0 and 1 = " echo |awk "$AWKSCRIPT" #如果省去echo,awk将缺少标准输入,需要手动输入 exit 0

    9.7 双圆括号结构

    ((…))与let命令很像,允许算术扩展和赋值。举个简单的例子a=$((5+3)),将把a设为"5+3"或者8.然而,双圆括号也是一种在Bash中允许使用C风格的变量处理的机制

    Example 9.30 C风格的变量处理

    #!/bin/bash #使用((...))处理一个C风格变量 echo (( a = 23 )) #给一个变量赋值,从"="号两边的空格就能看出这是C风格的处理 echo "a (initial value) = $a" (( a++ )) #变量a后+1,C风格 echo "a (after a++) = $a" (( a-- )) #变量a后-1,C风格 echo "a (after a--) = $a" (( ++a )) #变量a预+1,C风格 echo "a (after ++a) = $a" (( --a )) #变量a预-1,C风格 echo "a (after --a) = $a" echo #注意:在C语言中,预减和后减操作会有些不同的副作用 n=1;let --n && echo "True" || echo "False" #False n=1;let n-- && echo "True" || echo "False" #True echo (( t = a<45?7:11 )) #C风格的3元操作 echo "If a <45,then t = 7,else t = 11." echo "t = $t" echo exit 0

    见Example 10-12

    Processed: 0.013, SQL: 9