本文是对Linux shell编程的一个学习比较,共包括如下章节的内容:
- 概述
- 基本语法
- 结构化语句
- 函数
- 小结
参考资料:一些常见的linux bash命令可参见《Linux常见Shell命令》。
一、概述
(一)基本概念
Linux shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行。
shell既是一种解释器,也是一种编程语言,我们除了可以在命令行下执行单条的shell命令外,如 echo , ls , cpd等命令。我们还可以编写shell程序,shell程序是一种脚本语言,同其它脚本语言一样,shell程序支持变量、结构化语句等特性,我们可以将代码保存到脚本文件中,后续调用执行该脚本。
linux的shell和linux版本一样,有很多种,常见的如:
- Bourne Shell(/usr/bin/sh或/bin/sh)
- Bourne Again Shell(/bin/bash)
- C Shell(/usr/bin/csh)
- K Shell(/usr/bin/ksh)
- Shell for Root(/sbin/sh)
各种shell支持的命令和编程语言语法是类似的,但也有些细微的差别。这些shell类型中,其中最常用的是bash(即Bourne Again Shell),bash因为易用和免费,Bash 在日常工作中被广泛使用。同时,Bash 也是大多数Linux 系统默认的Shell。
当我们在Linux系统本机或远程打开一个linux控制台后,会自动运行默认的shell程序,出现交互式界面,我们可以通过echo $0 来查看当前运行的是哪种shell。如下面例子:
$ echo $0
-bash
上面例子显示,我们使用的是bash类型。
在本文,我们介绍的是bash。
(二)shell脚本
下面我们来看下如何编写和执行一个shell脚本文件。
1、新建一个文本文件
文件名可以是任意合法的文件名,扩展名习惯为sh,当然也可以没有扩展名。我们这里假设文件名为test.sh。
文件内容如下:
#!/bin/bash
echo hello
上面例子的脚本文件就两行,第一行是个特殊语句,#! 是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell,我们这里是使用bash。所有的shell脚本都必须有这一行内容。
后面的内容就可以是命令了,我们这里只有一行命令,即echo命令。这样执行该脚本,会在控制台输出hello内容。
2、设置可执行权限
脚本要想当作可执行文件被运行,需要设置权限,如下方式:
chmod +x ./test.sh
使用chmod命令使test.sh文件变为可执行程序。
3、执行
在shell提示符下运行 ./test.sh ,就可以执行该脚本。
在bash脚本中,除#!这个外,所有#后面的内容都会被当作注释内容,执行时会被忽略,如下面脚本:
#!/bin/bash
echo hello #输出hello信息
#定义shell变量
myid=100
#输出变量内容
echo $myid
上面脚本中,第一行#!打头的用于指定shell类型,后面的#都是用来书写注释信息的。
shell脚本程序实际是由一条条的命令和结构化语句组成,我们可以把这一条条的shell命令调用看作其它编程语言的函数调用,另外bash也支持定义函数。linux系统常见的shell命令可参考《Linux常见Shell命令》。
(三)脚本参数
在编写shell脚本程序时,很多情况下程序需要获取一些用户输入的信息,这有两种方式,一种是在脚本中使用read命令,当脚本执行到read命令时,会停下等待用户输入后才会继续执行;但在很多时候,脚本往往被自动执行,不适合用户输入,这个可以通过调用脚本时给脚本传入参数来实现,这种场景往往更多。
下面我们来介绍如何给脚本传参数。
在执行 Shell 脚本时,向脚本传递参数,脚本内获取参数的格式为:$n。n 代表一个数字,1 为执行脚本的第一个参数,2 为执行脚本的第二个参数,以此类推……。另外$0 为执行的文件名。
下面举例说明:
假设脚本文件名为test.sh,文件内容如下:
#!/bin/bash
echo $0
echo $1
echo $2
执行如下的命令:
$ ./test.sh tom 20
./test.sh
tom
20
可以看出,我们在调用test.sh传入了两个参数tom和20,在脚本程序中通过2获取到传入的参数,并且通过$0获取到文件名。
为了简单,本文下面的例子演示没有写到脚本文件中进行调用执行,而是直接在控制台$符号下进行的。
二、基本语法
(一)shell变量
变量是任何一门编程语言中的基本特性,变量用于保存值。Linux shell同样支持变量。在shell脚本中,可以创建变量,shell脚本中的变量只在创建它的shell中使用。shell中的变量创建和赋值放在一起,不需要使用任何关键字,只需”变量名=变量值”即可。特别要注意的是,=两边不能有空格。
使用变量时,通过$符号来引用。如下面示例:
$ x=hello
$ echo $x
hello
$ y=12
$ echo $y
12
需要注意的是,上面例子中以符号是shell终端的提示符,该行语句表示执行一条命令。没有$开头行的是命令执行的输出内容。后面的例子一样。
上面示例代码中,第1行定义了变量x,其值为hello。第2行执行echo命令在控制台输出变量x的值,可以看出,引用变量时要在变量名前加$符号。第3行定义了变量y,第4行调用echo命令输出变量y的值。
shell是一种脚本语言,弱类型的,定义变量时不需要声明数据类型。赋值给变量的值全部会当作字符串处理。值可以用双引号或单引号括起来,也可以不用(如上面例子没有用引号括起),如下面例子:
$ x="hello"
$ echo $x
hello
$ x='hello"world'
$ echo $x
hello"world
上面例子中,给变量赋值时,值用引号括起来。
需要注意的是,在shell中,标识符(如变量名)是区分大小写的。
(二)环境变量
Linux下的环境变量与windows下的的环境变量类似。当linux内核启动一个程序时,内核会将一个包含字符串数组的列表传递给程序,这个列表称为命令执行环境,简称为环境,以name=value格式保存了一系列名/值对。
在bash中,我们可以使用这些环境变量。
linux中的环境变量分为持久和临时的,持久的环境变量是在文件中保存的,因为是持久化的,所以永久有效;而临时的环境变量只在内存中,只针对当前shell会话有效。
下面我们先看下如何定义临时的环境变量。通过export命令可以定义环境变量。如下面例子:
$ export myid=100
$ echo $myid
100
上面例子第一句定义了环境变量myid,该变量的值为100,注意等号两边不能有空格。使用环境变量和使用shell变量一样,用$加前缀引用。
在上一节我们介绍了shell变量的使用,那与环境变量有啥区别呢?我们通过例子来说明:
$ myid=100
$ echo $myid
100
$ bash
$ echo $myid
$
上面例子,我们先定义了一个shell变量myid,然后我们在当前会话下运行bash程序(相当于启动了一个子shell),会发现在这子shell中,无法获取到父shell中定义的变量。
我们看下如果定义成环境变量会如何?如下面例子:
$ export myid=100
$ echo $myid
100
$ bash
$ echo $myid
100
$
可以看出,定义成环境变量后,在子shell中就可以使用。
需要说明的是,当在控制台执行一个shell脚本文件时,相当于启动一个子shell,这意味着无法在shell文件中引用外部定义的shell变量,但可以使用环境变量。
下面我们来看下如何持久化环境变量,即将环境变量的设置信息保存在文件中。Linux中环境变量包括系统级和用户级,系统级的环境变量是每个登录到系统的用户都要读取的系统变量,而用户级的环境变量则是该用户使用系统时加载的环境变量。 所以linux保存环境变量的文件也分为系统级和用户级的。
持久化环境变量,很简单,只需将export命令写入到相应的配置文件中。需要说明的是,不同版本的linux系统,所使用的配置文件可能不同,而且这些配置文件还存在相互调用的问题。
用户登陆时,会先执行/etc/profile脚本文件中的命令,为全系统所有用户建立环境变量。我们可以在该文件中设置一些对所有用户都有关的环境变量。需要注意的是,如果修改了该文件中的内容,需要重启系统才能生效。
每个用户特有的环境变量,在该用户的主目录下的脚本中设置。如.bash_profile脚本文件,该文件会调用.bashrc脚本文件,而.bashrc又会调用/etc/bashrc脚本文件。我们可以在.bash_profile或.bashrc中设置本用户特有的环境变量。每次打开shell,.bash_profile都会被执行。如果修改了.bash_profile或.bashrc文件中的内容,需要退出当前shell重新打开shell才能生效。
用户主目录下还有一个脚本文件.bash_logout,当用户退出当前shell时,该脚本会被执行,可以在该脚本中进行一些资源释放操作。
(三)运算符与表达式
表达式是将常量、变量、运算符组合在一起,产生一个值。在本小节我们来介绍算术运算。bash支持的算术运算符和其它语言差不多,包括 +(加)、-(减)、*(乘)、/(整除)、%(取余)
在bash中,与其它语言不同,如下的操作是无法进行算术运算的,如:
$ a=2+3
$ echo $a
2+3
$ a=2 + 3
bash: +: 未找到命令...
上面例子,直接数字相加不是一个表达式,如果+两边无空格,则会被当作一个字符串;如果+两边有空格,则会报错。
我们再看下,有变量参与的情况,例子如下:
$ a=2
$ b=$a+2
$ echo $b
2+2
$ b=$a + 3
bash: +: 未找到命令...
从执行情况看,与常量运算差不多,依然无法进行计算。
在bash中,算术表达式需要有些特殊的写法,第一种方式是利用expr表达式,如下面例子:
$ echo `expr 2 + 2`
4
$ a=3
$ echo `expr $a + 2`
5
也就是说要在表达式前加expr关键字,并将整个内容放在倒引号之中,这才是一个合法的算术表达式。另外要特别注意的是,运算符左右要有空格,没有空格就不会被计算,如下面例子:
$ echo `expr 2+2`
2+2
可以看出,上面例子+左右无空格,被当作了一个字符串。这使用起来比较别扭。
还有一种方式是,使用 $[表达式] 这样的方式,如:
$ echo $[3+5]
8
采用$[]方式的好处是,运算符,如上面的+左右可以有空格,也可以没空格,这个与其它编程语言一样,用起来就不容易出错。
另外使用符号,这样就与其它编程语言中的算术表达式类似,如下面例子:
$ a=12
$ echo $[a-4]
8
$ echo $[a*a]
144
通过内置命令let可以将一个表达式赋值给一个变量,变量的值就是表达式的计算结果,如下面例子:
$ let x=12+14
$ echo $x
26
上面例子,通过内置命令let将一个算术表达式赋值给了一个变量x。可以看出,变量的值就是表达式的计算结果。需要注意的是,=和+两边都不能有空格。
从上面几种算术表达式的编写方式看,还是使用$[]格式最好,不需要关心是否有空格等问题。
需要注意的是:标准的bash只支持整数运算,不支持浮点运算,如下面例子:
$ echo $[12+12]
24
$ echo $[12+12.5]
-bash: 12+12.5: 语法错误: 无效的算术运算符 (错误符号是 ".5")
可以看出,上面的表达式中有浮点数12.5,会报错。如果想要支持浮点运算,需要使用特殊的第三方包,这里不再介绍。
(四)字符串连接
连接字符串是个常见的操作,不同于很多编程语言,通过+运算符可以将字符串连接,在bash中,连接字符串的方式如下:
$ x=hello
$ y=world
$ z=${x}${y}
$ echo $z
helloworld
从上面例子可以看出,连接字符串时,用{}将变量名括起,注意$符号要放在{}外边。下面再看一个将变量与常量连接的例子:
$ x=hello
$ z=${x}world
$ echo $z
helloworld
(五)数组
bash支持一维数组,将多个空格分开的值放在()可定义数组,利用下标(从0开始)可获取数组的中等个元素。如下面例子:
$ arr=(a b c)
$ echo ${arr[0]}
a
$ echo ${arr[2]}
c
上面例子,先定义了一个数组arr,然后利用 ${数组名[序号]} 这样的方式来获取数组中的元素。
我们还可以利用下标来新建元素或往元素中添加元素,如:
myarr[0]=1
上面语句,如果myarr数组不存在,则会新建一个数组,并添加一个元素。如果已经存在,且指定序号的元素存在,则会替换,bash中的元素序号可以不连续,如:
myarr[5]=2
利用@或*做下标可以获取所有数组的内容,如下面例子:
$ data=( 2 4)
$ echo ${data[*]}
2 4
$ echo ${data[@]}
2 4
获取数组长度的语法是 ${#数组名{*}},如:
echo ${#data[*]}
三、结构化语句
(一)条件表达式
条件表达式主要用于if条件语句和循环语句中。
bash的条件表达式有如下写法,一种是使用[]的条件表达式,如下面例子:
a=3
b=4
if [ $a == $b ]
then
echo a==b
else
echo a!=b
fi
上面例子中[]之间是一个条件表达式,==是判断是否相等的运算符。需要注意的是,[]内部的两边必须要有空格,运算符==的两边也必须要有空格。这是使用bash编写程序比较别扭和需要注意的地方。
下表列出了bash常用的关系运算符,假定变量 a 为 3,变量 b 为 4:
运算符 | 说明 | 举例 |
---|---|---|
== | 检测两个数是否相等,相等返回 true。 | b 返回false |
!= | 检测两个数是否不相等,相等返回 true。 | b 返回true |
-eq | 检测两个数是否相等,相等返回 true。等同于== | [ b ] 返回 false。 |
-ne | 检测两个数是否不相等,不相等返回 true。等同于!= | [ b ] 返回 true。 |
-gt | 检测左边的数是否大于右边的,如果是,则返回 true。 | [ b ] 返回 false。 |
-lt | 检测左边的数是否小于右边的,如果是,则返回 true。 | [ b ] 返回 true。 |
-ge | 检测左边的数是否大于等于右边的,如果是,则返回 true。 | [ b ] 返回 false。 |
-le | 检测左边的数是否小于等于右边的,如果是,则返回 true。 | [ b ] 返回 true。 |
bash也支持逻辑运算符,对两个布尔值进行运算,其中 -a 用于“与”运算;-o用于“或”运算;-o用于“非”运算,如下面例子:
a=3
b=4
if [ $a -lt 5 -a $b -gt 2 ]
then
echo "a 小于 5 且 b 大于 2 "
fi
bash也支持字符串的布尔运算。下表列出了常用的字符串运算符,假定变量 a 为 "abc",变量 b 为 "efg":
运算符 | 说明 | 举例 |
---|---|---|
= | 检测两个字符串是否相等,相等返回 true。 | [ b ] 返回 false。 |
!= | 检测两个字符串是否相等,不相等返回 true。 | [ b ] 返回 true。 |
-z | 检测字符串长度是否为0,为0返回 true。 | [ -z $a ] 返回 false。 |
-n | 检测字符串长度是否为0,不为0返回 true。 | [ -n "$a" ] 返回 true。 |
$ | 检测字符串是否为空,不为空返回 true。 | [ $a ] 返回 true。 |
在shell编程中,经常要对文件进行操作。bash提供了很多运算符进行文件的相关测试。相关运算符如下:
- -e 文件名 如果文件存在则为真
- ** -r 文件名 如果文件存在且可读则为真**
- -w 文件名 如果文件存在且可写则为真
- -x 文件名 如果文件存在且可执行则为真
- -s 文件名 如果文件存在且至少有一个字符则为真
- -d 文件名 如果文件存在且为目录则为真
- -f 文件名 如果文件存在且为普通文件则为真
- -c 文件名 如果文件存在且为字符型特殊文件则为真
- -b 文件名 如果文件存在且为块特殊文件则为真
下面看一个例子:
if [ -e test.txt ]
then
echo test.txt is exist
fi
上面代码测试了当前目录下是否存在test.txt文件。
除了使用[]来表示条件表达式外,还可以使用test命令。test是bash的内置命令,test命令用于检查某个条件是否成立,它可以进行数值、字符和文件三个方面的测试,如果成立(即为真)。如下面例子:
a=3
b=4
if test $a == $b
then
echo a==b
else
echo a!=b
fi
对于数值判断,还支持通过(())来进行条件描述,并支持 > ,>= 等运算符,这些运算符[]和test命令不支持,如下面例子:
int=1
while(($int<=5))
do
echo $int
int=$[int+1]
done
使用(())这种写法,运算符,如上面的<=左右是否有空格无所谓。
(二)if语句
bash中的if条件语句,除细微的语法细节不同外,和c,java语言的if语句基本类似。其基本语法格式如下:
if 条件
then
命令...
fi
如果条件为真,则会执行then后的命令(可以有1条或多条命令),if语句以fi结尾。
除 if...then...fi语句外,bash还支持 if ...then...else..fi语句,即当if后的条件为真,执行then后的语句,否则执行else后的语句。
另外还支持 if ..then..[elif ..then]..else..fi 语句,可以有1个或多了elif语句。
因为if语句的语法和其它编程语言类似,上面小节也有举例了,所以不再详细介绍。
(三)case语句
类似c,java中的case语句,用于支持多条件匹配,其语法格式如:
case 值 in
匹配值1)
command1
command2
...
commandN
;;
...
匹配值n)
command1
command2
...
commandN
;;
esac
上面语法有如下要求:
1)case和in之间的值可以是常量或变量
2)每个匹配值后面跟着)右括号
3)某个匹配值被匹配,其后的命令被执行,直至碰到;; (相当于其它语言中的break)。需要说明的是,被匹配的到分支,一定要有;;x
4)如果想匹配任意值,匹配值可写成*,一般放到最后
5)最后整个case语句需要用esac结尾(即case单词反过来写)。
下面我们看一个具体的例子:
num=2
case $num in
1) echo 'num = 1'
;;
2) echo 'num = 2'
;;
3) echo 'num = 3'
;;
4) echo 'num = 4'
;;
*) echo 'num = other'
;;
esac
执行上面代码,发现会输出2.
(四)for循环
bash支持for循环,有如下用法.
1、遍历多个值,如:
$ for v in a b c
> do
> echo $v
> done
a
b
c
上面例子用到了for in do done 四个关键字,in后面跟的是要遍历的列表值,for和in之间是用于代表每次循环元素,执行的命令需要位于 do和done之间。
说明:上面的for语句有换行时,>是控制台自动显示的内容。如果是在脚本中编写,则没有>符号。也可以写在一行,采用如下格式:
for v in a b c; do echo $v; done
2、遍历数组
可以用for语句来遍历数组,如下面例子:
$ for v in ${arr[*]}
> do
> echo $v
> done
3
6
12
3、c语言的风格
也支持类似c语言的for循环风格,如下面例子:
$ for((i=1;i<=5;i++))
> do
> echo $i
> done
1
2
3
4
5
注意,for后面的内容是用两个小括号包围的。
(五)while循环
类似c,java中的while语句,满足条件执行循环中的内容。如下面例子:
int=1
while [ $int -le 5 ]
do
echo $int
int=$[int+1]
done
或改为(())的条件写法
int=1
while(( $int<=5 ))
do
echo $int
let int=int+1hu
done
注意,while后面的条件表达式要放在两个()之间。
同样我们可以使用test内置命令来作为条件,如:
int=1
while test $int -le 5
do
echo $int
let int=int+1
done
(六)break和continue语句
类似c,java中的break和continue,可以跳出循环,或提前结束本次循环。下面看具体例子:
int=1
while [ $int -le 5 ]
do
if (( $int==3 ))
then
break
fi
echo $int
int=$[int+1]
done
上面代码执行,只会输出1,2信息。
再看一个continue的例子:
int=0
while [ $int -le 5 ]
do
int=$[int+1]
if (( $[int%2]==0 ))
then
continue
fi
echo $int
done
上面代码执行,只会输出1,3,5信息。
四、函数
(一)基础
bash支持定义函数,其语法格式为:
[ function ] 函数名 ()
{
命令
[return 整数]
}
上面语法有如下特点:
1)关键字function可选
2)return语句可选,return后面跟0~255的整数值,作为函数返回值。如果无return语句,则以最后一条命令运行结果作为返回值。
下面我们通过具体的例子来说明。我们先来看一个最简单的例子,如:
myfun()
{
echo "this is a fun"
}
上面定义了一个函数,函数名为myfun。
调用函数就同调用shell命令类似,注意不像其它语言,调用时不能加(),如:
$ myfun
this is a fun
上面在$提示符下直接输入myfun就是调用函数。
(二)函数的返回值
我们先看下shell命令执行的返回值,bash中的每条命令执行后都会有一个返回值,一般情况下成功返回值为0,出错的话返回值就是一个非0整数。命令执行的返回值,通过在命令执行后使用$?来获取。
如下面例子:
$ ls test.txt
test.txt
$ echo $?
0
$ ls test5.txt
ls: 无法访问test5.txt: 没有那个文件或目录
$ echo $?
2
$ ls test.txt
test.txt
$ echo $?
0
从上面例子我们可以看出,先使用ls命令显示一个正确的文件后,$?的值为0,这是命令返回结果;然后用ls命令显示不存在的文件,ls命令会报错,这时$?的值为非零值。
bash函数的调用,和执行shell命令类似,其返回值也是通过$?来获取的,这点与其它编程语言差别很大。
前面我们提到,在函数中通过可选的return语句,后面跟0~255的整数值,作为函数返回值。如果无return语句,则以最后一条命令运行结果作为返回值。
下面我们通过例子来说明:
$ myfun()
> {
> echo "this is a fun"
> return 100
> }
$ myfun
this is a fun
$ echo $?
100
上面例子我们先定义了一个函数myfun,然后调用该函数,最后显示$?的值,这个值就是myfun函数中的return后面的值。
下面我们再看一个无return语句的例子,如:
$ myfun()
> {
> rm other.txt
> echo hello
> }
$ myfun
rm: 无法删除"other.txt": 没有那个文件或目录
hello
$ echo $?
0
上面例子中定义的myfun函数中有两条命令,调用函数时,rm命令报错,echo命令正常执行,所以调用函数后获取到的$?的值为0。如果我们试着把这两个命令顺序换下,会发现$?的值不为0,这里不再具体的演示。
(三)函数的参数
Bash的函数也可以有参数,但参数的使用方法和别的编程语言有较大差别,c/java等编程语言的函数参数是在函数声明中定义,而bash函数的参数由调用时决定。
在调用bash函数时,可以向其传递参数。在函数体内部,通过 $n 的形式来获取参数的值,例如,$1表示第一个参数,$2表示第二个参数,下面我们通过例子来说明:
$ myfun()
> {
> echo $1
> echo $2
> }
$ myfun hello 2
hello
2
上面代码中函数myfun通过$1和$2引用了两个函数参数,调用函数时,传入了2个参数,正好对应$1和$2.
如果我们调用时不传入参数值,则函数内的$n的值为空。
结合前面所学的,可以看出,给函数传递参数和给脚本文件传递参数适用方式类似。
五、小结
本文是对linux shell编程的一个学习总结,linux shell是一种脚本语言,具有脚本语言的一些基本特征,如弱类型、解释执行,对变量、结构化语句的支持等,也有一些差异化的语法特点。