如果变量使用恰当,将会增加脚本的能量和灵活性。但前提是这需要仔细学习变量的细节知识。
$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配合使用,设置每次一完整读入的最小字符数为
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)
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
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.3节,h和expr命令列表的相关章节。
关于脚本的例子,见:
Example 12-9
Example 9-16
Example 9-17
Example 9-18
Example 9-20
${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" #同上
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 #输出空
假设一个变量的值是另一个变量的名字。我们有可能从第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并不支持指针的算术运算,并且这严格的限制了间接引用的使用。事实上,在脚本语言中,间接引用本来就是丑陋的部分
$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
((…))与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