2.2.3 变量和运算
变量是放置在内存中的某个存储单元,这个存储单元里存放的是这个单元的值,这个值是可以改变的,我们称之为变量。
其中,本地变量是在用户现有的Shell生命周期的脚本中使用的,用户退出后变量就不存在了,该变量只用于该用户。
下面都是跟变量相关的命令,这里只是大致地说明下,后面的内容里会有详细的说明,如下所示:
变量名="变量"
readonly 变量名="变量"表示设置该变量为只读变量,这个变量不能被改变。
echo $变量名
set 显示本地所有的变量
unset 变量名 表示清除变量
readonly 显示当前Shell下有哪些只读变量
环境变量用于所有用户进程(包括子进程)。Shell中执行的用户进程均称为子进程。不像本地变量只用于现在的Shell。环境变量可用于所有的子进程,它包括编辑器、脚本和应用。
环境变量主目录如下:
$HOME/.bash_profile(/etc/profile)
设置环境变量,例句如下:
export test="123"
查看环境变量,命令如下:
env
或者用如下命令:
export
本地变量中包含环境变量。环境变量既可以运行于父进程,也可以运行于子进程中。本地变量则不能运行于所有的子进程中。
变量清除命令如下:
unset 变量名
再来看看位置变量,在运行某些程序时,程序中会带上一系列参数,若我们要用到这些参数,则会采用位置来表示,对于这样的变量,我们称之为位置变量,目前在Shell中的位置变量有10个($0~$9),超过10个则用其他方式表示,其中,$0表示整个Shell脚本,这点要记住。
下面举例说明位置变量的用法。比如,有如下test.sh脚本内容:
#!/bin/bash
echo "第一个参数为": $0"
echo "第二个参数为": $1"
echo "第三个参数为": $2"
echo "第四个参数为": $3"
echo "第五个参数为": $4"
echo "第六个参数为": $5"
echo "第七个参数为": $6"
现在给予test.sh执行权限,命令如下:
chmod +x test.sh
./test.sh 1 2 3 4 5 6
命令结果显示如下:
第一个参数为: ./test.sh
第二个参数为: 1
第三个参数为: 2
第四个参数为: 3
第五个参数为: 4
第六个参数为: 5
第七个参数为: 6
值得注意的是,从第10个位置参数开始,必须使用花括号括起来,如:${10}。特殊变量$*和$@表示所有的位置参数。特殊变量$#表示位置参数的总数。
下面我们进一步详细说明下Shell的知识要点。
1.运行Shell脚本
Shell脚本有两种运行方式,第一种方式是利用sh命令,把Shell脚本文件名作为参数。这种执行方式要求Shell脚本文件具有“可读”的访问权限,然后输入sh test.sh即可执行。
第二种执行方式是利用chmod命令设置Shell脚本文件,使Shell脚本具有“可执行”的访问权限。然后直接在命令提示符下输入Shell脚本文件名,例如./test.sh。
2.调试Shell脚本
使用bash -x 可以调试Shell脚本,bash会先打印出每行脚本,再打印出每行脚本的执行结果,如果只想调试其中几行脚本,可以用set -x和set +x把要调试的部分包含进来,命令如下:
set -x
脚本部分内容
set +x
这个时候可以直接运行脚本,而不需要再执行 bash -x 了。这个功能在实际工作中非常有用,可以帮助我们调试变量,找出bug点,总之是非常有用的功能,希望大家掌握。
3.退出或出口状态
一个Unix进程或命令运行终止时,将会自动地向父进程返回一个出口状态。如果进程成功执行完毕,将会返回一个数值为0的出口状态。如果进程在执行过程中出现异常而未能正常结束时,将会返回一个非零值的出错代码。
在Shell脚本中,可以利用“exit[n]”命令在终止执行Shell脚本的同时,向调用脚本的父进程返回一个数值为n的Shell脚本出口状态。其中,n必须是一个位于0~255范围内的整数值。如果Shell脚本是以不带参数的exit语句结束执行的,则Shell脚本的出口状态就是脚本中最后执行的那条命令的出口状态。
在Unix系统中,为了测试一个命令或Shell脚本的执行结果,$?内部变量将返回之前执行的最后一条命令的出口状态,这些状态中,0才是正确值,其他非零的值都表示是错误的。
4. Shell变量
Shell变量名可以由字母、数字和下划线等字符组成,但第一个字符必须是字母或下划线。
Shell中的所有变量都是字符串类型的,它并不区分变量的类型,如果变量中包含下划线(_)的话,就要注意了,有些脚本的区别就很大,比如脚本中$PROJECT_svn_$DATE.tar.gz与${PROJECT}_svn_${DATE}.tar.gz的区别就很大,注意变量${PROJECT_svn},如果不用{}将变量全部包括的话,Shell则会理解成变量$PROJECT,后面再接着_svn。
从用途上考虑,变量可以分为内部变量、本地变量、环境变量、参数变量和用户自定义的变量,下面分别说明它们各自的定义
内部变量是为了便于Shell编程而由Shell设定的变量。如错误类型的ERRNO变量。
本地变量是在代码块或函数中定义的变量,且仅在定义的范围内有效。
参数变量是调用Shell脚本或函数时传递的变量。
环境变量是为系统内核、系统命令和用户命令提供运行环境而设定的变量。
用户自定义的变量是为运行用户程序或完成某种特定的任务而设定的普通变量或临时变量。
5.变量的赋值
变量的赋值可以采用赋值运算符=来实现,其语法格式如下:
variable=value
注意,赋值运算符前后不能有空格,否则会报错,写惯了Python后再回头写Shell脚本就会经常犯这种错误;未初始化的变量值为null,使用如下变量赋值的形式,即可声明一个未初始化的变量。
如果variable=value赋值运算符前后有空格,则会出现如下报错信息:
err = 72
-bash: err: command not found
写惯了Python程序后再回头写Shell脚本,笔者也经常也会犯这种错误,大家不要忘了,Shell的语法其实也是很严谨的。
6.内部变量
Shell提供了丰富的内部变量,为用户的Shell编程提供各种支持。内部变量及其意义如下所示。
PWD:表示当前的工作目录,其变量值等同于PWD内部命令的输出。
RANDOM:每次引用这个变量时,将会生成一个均匀分布的0~32 767范围内的随机整数。
SCONDS:脚本已经运行的时间(秒)。
PPID:当前进程的父进程的进程ID。
$?:表示最近一次执行的命令或Shell脚本的出口状态。
7.环境变量
主要环境变量及其意义如下所示。
EDITOR:用于确定命令行编辑所用的编辑程序,通常为vim。
HOME:用户主目录。
PATH:指定命令的检索路径。
例如,要将/usr/local/mysql/bin目录添加到系统默读的PATH变量中,应该如何操作呢?
PATH=$PATH:/usr/local/mysql/bin
export PATH
echo $PATH
如果想让其重启或重开一个Shell也生效,又该如何操作呢?
Linux中包含了两个重要的文件: /etc/profile和$HOME/.bash_profile,每次系统登录时都要读取这两个文件,用来初始化系统所用到的变量,其中/etc/profile是超级用户所用的,$HOME/.bash_profile是每个用户自己独立的,可以通过修改该文件来设置PATH变量。
这种方法只能使当前用户生效,并非所有用户。
如果要让所有用户都能够用到此PATH变量,可以用vim命令打开/etc/profile文件,并在适当位置添加PATH=$PATH:/usr/local/mysql/bin,然后执行source /etc/profile使其生效。
8.变量的引用和替换
假定variable是一个变量,在变量名字前加上“$”前缀符号,即可引用变量的值,表示使用变量中存储的值来替换变量名字本身。
引用变量有两种形式:$variable与${variable}。
位于双引号中的变量可以进行替换,但位于单引号中的变量则不能进行替换。
9.变量的间接引用
假定一个变量的值是另一个变量的名字,那么根据第一个变量可以获得第三个变量的值。举例说明如下:
a=123
b=a
eval c=\${$b}
echo $b
echo $c
实际工作中不推荐使用这种用法,因为写出来的脚本容易产生歧义,让人混淆,而且也不方便在团队里面交流工作。
10.变量声明与类型定义
尽管Shell并未严格地区分变量的类型,但在Bash中,可以使用typeset或declare命令定义变量的类型,并在定义时进行初始化。
11.部分常用命令介绍
这里将介绍工作中常用的部分Shell命令,如下所示。
(1)冒号
冒号(:)与true语句不执行任何实际的处理动作,但可用于返回一个出口状态为0的测试条件。这两个语句常用于while循环结构的无限循环测试条件,在脚本中经常会见到这样的用法:
while :
这表示是一个无限循环的过程,所以使用的时候要特别注意,不要成了死循环,所以一般会定义一个sleep时间,可以实现秒级别的cron任务,其语法格式如下:
while :
do
命令语句
sleep 自己定义的秒数
done
(2)echo与print命令
print的功能与echo的功能完全一样,主要用于显示各种信息。
(3)read命令
read语句的主要功能是读取标准输入的数据,然后存储到变量参数中。如果read命令的后面有多个变量参数,则输入的数据会按空格分隔的单词顺序依次为每个变量赋值。read在交互式脚本中相当有用,建议大家掌握。
read命令用于接收标准输入设备(键盘)的输入,或其他文件描述符的输入(后文再详细说明)。得到输入后,read命令将数据放入一个标准变量中。下面是read命令的最简单形式:
#!/bin/bash
echo -n "Enter your name:" #参数-n的作用是不换行,echo默认是换行
read name #从键盘输入
echo "hello $name,welcome to my program" #显示信息
exit 0 #退出Shell程序。
由于read命令提供了-p参数,允许在read命令行中直接指定一个提示,因此上面的脚本可以简写成下面的脚本:
#!/bin/bash
read -p "Enter your name:" name
echo "hello $name, welcome to my program"
exit 0
(4)set命令
set命令用于修改或重新设置位置参数的值。Shell规定,用户不能直接为位置参数赋值。使用不带参数的set将会输出所有的内部变量。
“set --”用于清除所有的位置参数。
(5)unset命令
该命令用于清除Shell变量,把变量的值设置为null。这个命令并不影响位置参数。
(6)expr命令
expr命令是一个手工命令行计数器,用于在Linux下求表达式变量的值,一般用于整数值,也可用于字符串。其格式为:
expr Expression
expr命令读入Expression参数,计算它的值,然后将结果写入到标准输出
Expression参数应用规则如下:
用空格隔开每个项。
用/(反斜杠)放在Shell的特定字符前面。
对于包含空格和其他特殊字符的字符串要用引号括起来。
expr命令支持的整数算术运算表达式如下:
exp1+exp2,计算表达式exp1和exp2的和。
exp1-exp2,计算表达式exp1和exp2的差。
exp1/*exp2,计算表达式exp1和exp2的乘积。
exp1/exp2,计算表达式exp1和exp2的商。
exp1%exp2,计算表达式exp1与exp2的余数。
另外expr命令还支持字符串比较表达式,语句如下:
str1=str2
该语句是比较字符串str1和str2是否相等,如果计算结果为真,则同时输出1,返回值为0;反之计算结果为假,则同时输出0,返回值为1。
要说明的是,expr默认是不支持浮点运算的,比如我们想在expr下面输出echo "1.2*7.8"的运算结果,那是不可能的,那么应该怎么办呢?这里可以用到bc,举例说明如下:
echo "scale=2;1.2*7.8" |bc
#这里的scale用来控制小数点精度,默认为1
(7)let命令
let命令取代并扩展了expr命令的整数算术运算。let命令除了支持expr支持的5种算术运算外,还支持+=、-=、*=、/=、%=。
12.数值常数
Shell脚本按十进制解释字符串中的数字字符,除非数字前有特殊的前缀或记号,若数字前有一个0则表示为一个八进制的数,0x或0X则表示为一个十六进制的数。
13.命令替换
命令替换的目的是获取命令的输出,且为变量赋值或对命令的输出做进一步的处理。命令替换实现的方法为采用$(...)的形式引用命令或使用反向引号引用命令'command'。如:
today=$(date)
echo $today
如果文件filename中包含需要删除的文件列表时,则采用如下命令:
rm $(cat filename)
14. test语句
test语句与if/then和case结构的语句一起,构成了Shell编程的控制转移结构。
test命令的主要功能是计算紧随其后的表达式,检查文件的属性、比较字符串或比较字符串内含的整数值,然后以表达式的计算结果作为test命令的出口状态。如果test命令的出口状态为真,则返回0;如果为假,则返回一个非0的数值。
test命令的语法格式有:test expression或[ expression ],注意方括号内侧的两边必须各有一个空格。
[[ expression ]]是一种比[ expression ]更通用的测试结构,也可用于扩展test命令。
15.文件测试运算符
文件测试主要指文件的状态和属性测试,其中包括文件是否存在、文件的类型、文件的访问权限及其他属性等。
下面各项为文件属性测试表达式。
-e file,如果给定的文件存在,则条件测试的结果为真。
-r file,如果给定的文件存在,且其访问权限是当前用户可读的,则条件测试的结果为真。
-w file,如果给定的文件存在,且其访问权限是当前用户可写的,则条件测试的结果为真。
-x file,如果给定的文件存在,且其访问权限是当前用户可执行的,则条件测试的结果为真。
-s file,如果给定的文件存在,且其大于0,则条件测试的结果为真。
-f file,如果给定的文件存在,且是一个普通文件,则条件测试的结果为真。
-d file,如果给定的文件存在,且是一个目录,则条件测试的结果为真。
-L file,如果给定的文件存在,且是一个符号链接文件,则条件测试的结果为真。
-c file,如果给定的文件存在,且是字符特殊文件,则条件测试的结果为真。
-b file,如果给定的文件存在,且是块特殊文件,则条件测试的结果为真。
-p file,如果给定的文件存在,且是命名的管道文件,则条件测试的结果为真。
常见代码举例如下:
BACKDIR=/data/backup
[ -d ${BACKDIR} ] || mkdir -p ${BACKDIR}
[ -d ${BACKDIR}/${DATE} ] || mkdir ${BACKDIR}/${DATE}
[ ! -d ${BACKDIR}/${OLDDATE} ] || rm -rf ${BACKDIR}/${OLDDATE}
下面是字符串测试运算符。
-z str,如果给定的字符串的长度为0,则条件测试的结果为真。
-n str,如果给定的字符串的长度大于0,则条件测试的结果为真。要求字符串必须加引号。
s1=s2,如果给定的字符串s1等同于字符串s2,则条件测试的结果为真。
s1!=s2,如果给定的字符串s1不等同于字符串s2,则条件测试的结果为真。
s1<s2,如果给定的字符串s1小于字符串s2,则条件测试的结果为真。
s1>s2,若给定的字符串s1大于字符串s2,则条件测试的结果为真。
在比较字符串的test语句中,变量或字符串表达式的前后一定要加双引号。
再来看看整数值测试运算符。test语句中整数值的比较会自动采用C语言中的atoi()函数把字符转换成等价的ASC整数值。所以可以使用数字字符串和整数值进行比较。整数测试表达式为:-eq(等于)、-ne(不等于)、-gt(大于)、-lt(小于)、-ge(大于等于)、-le(小于等于)。
16.逻辑运算符
Shell中的逻辑运算符及其意义如下所示:
(expression):用于计算括号中的组合表达式,如果整个表达式的计算结果为真,则测试结果也为真。
!exp:可对表达式进行逻辑非运算,即对测试结果求反。例如:test ! -f file1。
符号-a或&&:表示逻辑与运算。
符号-o或||:表示逻辑或运算。
Shell脚本中的用法可参考图2-1。
图2-1 &&与||指令说明
17. Shell中的自定义函数
自定义函数语法比较简单,如下:
function 函数名()
{
action;
[return 数值;]
}
具体说明如下:
自定义函数既可以用带function参数的函数名()定义,也可以直接用函数名()定义,而不用带任何参数。
参数返回时,可以显式地加return返回,如果不加,则将以最后一条命令的运行结果作为返回值。return后跟数值,取值范围0~255。
举例说明,遍历/usr/local/src目录里面包含的所有文件(包括子目录),脚本内容如下:
#!/bin/bash
function traverse(){
for file in 'ls $1'
do
if [ -d $1"/"$file ]
then
traverse $1"/"$file
else
echo $1"/"$file
fi
done
}
traverse "/usr/local/src"
18. Shell中的数组
Shell是支持数组的,但仅支持一维数组(不支持多维数组),并且没有限定数组的大小。类似于C语言,数组元素的下标由0开始编号。获取数组中的元素要利用下标,下标可以是整数或算术表达式,其值应大于或等于0。
(1)定义数组
在Shell中,用括号来表示数组,数组元素之间用空格符号分隔开。定义数组的一般形式为:
array_name=(value1 ... valuen)
例如:
array_name=(value0 value1 value2 value3)
或者:
array_name=(
value0
value1
value2
value3
)
还可以单独定义数组的各个分量:
array_name[0]=value0
array_name[1]=value1
array_name[2]=value2
可以不使用连续的下标,而且下标的范围没有限制。
(2)读取数组
读取数组元素值的一般格式为:
${array_name[index]}
例如:
valuen=${array_name[2]}
下面用一个Shell脚本举例说明上面的用法,脚本内容如下所示:
#!/bin/bash
NAME[0]="yhc"
NAME[1]="cc"
NAME[2]="gl"
NAME[3]="wendy"
echo "First Index: ${NAME[0]}"
echo "Second Index: ${NAME[1]}"
运行脚本,命令如下所示:
bash ./test.sh
输出结果如下所示:
First Index: Zara
Second Index: Qadir
使用@或*可以获取数组中的所有元素,例如:
${array_name[*]}
${array_name[@]}
对上面的代码加上最后两行,如下所示:
echo "${NAME[*]}"
echo "${NAME[@]}"
运行脚本,输出:
First Index: yhc
Second Index: cc
yhc cc gl wendy
yhc cc gl wendy
(3)获取数组的长度
获取数组长度的方法与获取字符串长度的方法相同,例如:
获取数组元素的个数,命令如下所示:
length=${#array_name[@]}
获取数组单个元素的长度,命令如下所示:
length=${#array_name[*]}
19. Shell中的字符串截取
Shell截取字符串的方法有很多,一般常用的有以下几种方法。
先来看第一种方法,从不同的方向截取。
从左向右截取最后一个string后的字符串,命令如下:
${varible##*string}
从左向右截取第一个string后的字符串,命令如下:
${varible#*string}
从右向左截取最后一个string后的字符串,命令如下:
${varible%%string*}
从右向左截取第一个string后的字符串,命令如下:
${varible%string*}
下面是第二种方法。
${变量:n1:n2}:截取变量从n1开始的n2个字符,组成一个子字符串。可以根据特定字符偏移和长度,使用另一种形式的变量扩展方式来选择特定的子字符串,例如下面的命令:
${2:0:4}
这种形式的字符串截断非常简便,只须用冒号分开来指定起始字符和子字符串的长度即可,工作中用得最多的也是这种方式。
还有第三种方法。
这里利用cut命令来获取后缀名,命令如下:
ls -al | cut -d "." -f2