shell相关概念
shell和shell脚本
shell是一个用 C 语言编写的程序,它是用户使用Unix/Linux的桥梁,用户的大部分工作都是通过shell完成的。shell虽然不是Unix/Linux系统内核的一部分,但它调用了系统核心的大部分功能来执行程序、建立文件并以并行的方式协调各个程序的运行。
而shell脚本,是一种为shell编写的脚本程序。shell脚本是解释执行的,不需要编译。
本文中的各shell脚本示例均是在OSX系统编写和执行,其它系统可能会有一些较小的差别。
shell脚本解释器
shell脚本的执行需要解释器,最常见的有sh和bash:
- sh
sh全称Bourne Shell,是UNIX最初使用的shell,在每种 UNIX 上都可以使用
- bash
bash全称Bourne Again Shell,是Linux默认的shell脚本解释器,是Bourne Shell的扩展
在编写shell脚本时,需要在脚本的第一行指定执行脚本的解释器,
在脚本的第一行需要指定执行脚本的解释器,如指定为sh:
1 | !/bin/sh |
或指定bash:
1 | !/bin/bash |
除了以上两种之外,还有一些其它常见的shell脚本解释器,如C Shell(/usr/bin/csh
)、K Shell(/usr/bin/ksh
)和Shell for Root(/sbin/sh
)等。
脚本的执行
对于shell脚本文件script.sh
:
1 | 首先为脚本增加执行权限 |
注释
注释分为单行注释和多行注释。单行注释用#
标记注释的开始:
1 | comment |
多行注释可以通过很多种方式实现,比较常用的一种如下:
1 | : ' |
变量
定义与使用
定义变量时,直接使用“变量名=变量值”,注意变量名与等号、等号与变量值之间不能有空格,如:
1 | variableName="value" |
变量名首个字符必须为大写或小写字母;中间不能有空格,可以使用下划线(_);不能使用标点符号;不能使用保留的关键字。
使用已经定义过的变量时,在变量名之前加$
符号即可(为了标记变量名的边界,需要使用大括号将变量名包起来):
1 | variableName="value" |
设为只读
将变量设置为只读,设置为只读的变量不能对其重新赋值:
1 | readonly variableName |
销毁
当不再使用一个变量时,即可将其销毁:
1 | unset variableName |
注意unset
不可作用于只读的变量。
操作环境变量
在shell脚本中可以查看或修改系统的环境变量,如:
1 | echo $PATH |
脚本执行参数
在执行shell脚本时,可以向脚本传递参数,跟在脚本名称后边,使用空格分隔,如:
1 | ./test.sh debug 1 666 |
表示执行脚本test.sh
,同时向脚本传递三个参数,依次为debug
、1
和666
。在脚本内部,可以通过$n
的形式获取脚本执行参数,对于上述命令,在test.sh
内部可获取到的$0
、$1
、 $2
和 $3
依次为:./test.sh
、 debug
、 1
和 666
。
除了使用$n
获取执行参数之外,还有一些其它的特殊变量:
$#
: 传递给脚本的执行参数的个数$*
: 以一个单字符串显示所有向脚本传递的参数。"$*"
将以"$1 $2 ... $n"
的形式输出所有参数$@
: 与$*
相同,"$@"
将以"$1" "$2" ... "$n"
的形式输出所有参数$$
: 脚本运行的当前进程ID号$!
: 后台运行的最后一个进程的ID号$-
: 显示Shell使用的当前选项$?
: 显示最后命令的退出状态,0表示没有错误,其他任何值表明有错误
字符串
字符串的表示
字符串可以用双引号或者单引号表示,两种方法表示的字符串略有不同:
使用双引号
双引号表示的字符串中,可以使用变量或转义字符,
1 | name="world" |
使用单引号
使用单引号时,单引号内部的变量和转义字符不会生效,而是保留字面内容:
1 | name='world' |
字符串的操作
获取长度
使用#
来获取字符串长度:
1 | string="hello" |
字符串拼接
其实是在字符串中包含字符串变量:
1 | str1="hello" |
提取子串
提取字符串具体有以下四种方式:
str:B
:指定起始位置,截取到最后str:B:L
:指定起止位置和截取长度str:0-B
:反向指定起始位置,截取到最后str:0-B:L
:反向指定起始位置,并指定向后截取的长度
1 | str="hello world" |
截取字符串
#
: 从左开始算起,截取第一个匹配的字符##
: 从左开始算起,截取最后一个匹配的字符%
: 从右开始算起,截取第一个匹配的字符%%
: 从右开始算起,截取最后一个匹配的字符
具体参看示例代码:
1 | str="http://www.hello.com/world.html" |
数组
bash仅支持一维数组(不支持多维数组),不限定数组的大小。数组元素的下标由0开始编号。获取数组中的元素要利用下标,下标可以是整数或算术表达式,其值应大于或等于0。
定义数组
可以直接声明多个值,也可以单独指定各个值:
1 | array1=(value0 value1 value2) |
使用数组
获取数组中的某个元素,直接方括号加下标:
1 | array=(value0 value1 value2) |
如需获取所有元素,可以使用[*]
或[@]
,具体参考下例:
1 | array=(value0 value1 value2) |
获取数组长度的方法与字符串相似,使用#
:
1 | array=(value0 value1 value2) |
echo和printf
在shell中两个命令echo
和printf
,都用于向屏幕显示信息,首先是echo
的用法:
echo
显示字符串
echo
用来显示字符串,当字符串使用双引号表示时,可包含转义字符或变量,字符串两端的双引号可以省略。如果想保持字符串内容原封不动输出,则用单引号表示字符串。某些情况下转义字符不能正常显示,则需要在echo
命令之后加上参数-e
启用转义:
1 | echo -e "abc\ndef\a" |
以上命令会输出两行字符abc
和def
,并且发出警告声(\a
)。
换行控制
默认每次执行echo
会在输出内容最后换行,如果在echo
命令之后跟随参数-n
则可以强制不换行。也可以使用以下的转义字符控制是否换行或调整光标位置,注意以下转义字符可能需要使用-e
参数来开启:
\b
删除前一个字符\c
最后不加上换行符号\f
换行但光标仍旧停留在原来的位置\n
换行且光标移至行首\r
光标移至行首,但不换行\t
插入tab\v
与\f
相同
显示命令执行结果
1 | echo `date` # 显示当前日期 |
结果定向至文件
1 | echo "hello" > file.txt |
printf
shell中的printf
命令与C语言中的相似,用于格式化输出,语法为:
1 | printf format-string [arguments...] |
如:
1 | printf "hello %s\n" "world" # hello world |
注意与C语言中不同的几处:
printf
命令不用加括号format-string
可以没有引号,但最好加上,单引号双引号均可参数多于格式控制符
%
时,format-string
可以重用,可以将所有参数都转换,后边给出例子参数少于格式控制符
%
时,那么%s
用空字符串(NULL
)代替,%d
用0
代替arguments
使用空格分隔,不用逗号
以下是几个例子:
1 | printf "=========== TEST 1 ===========\n" |
以上命令输出为:
1 | =========== TEST 1 =========== |
运算符
shell本身不支持运算操作,但是可以借助expr
命令来实现:
使用expr命令
使用expr
命令时,需要将整个语句用(`)括起来,以下是一个简单的例子:
1 | echo `expr 1 + 1` |
算术运算
在使用expr
进行算术运算时,注意运算数与运算符之间必须要有一个空格,否则会语法错误。以下是一些例子:
1 | a=25 |
以上执行结果为:
1 | a + b : 35 |
使用let命令
let
是用于计算的语句,可用于执行一个或多个表达式,变量计算中不需要加上$
来表示变量。以下是一个示例:
1 | let a=10+25 |
除此之外,还可以通过let
来实现变量的自增和自减:
1 | num=1 |
关系比较
用于比较关系的表达式必须用方括号括起来,且注意运算数与运算符之间必须要有一个空格。
在shell中,用于比较的关系的运算符有符号型的和字母型的两类,字母型的比较运算符适用于数值的比较,参考以下示例:
1 | a=10 |
符号型的运算符可用来比较数值,也可以用来比较字符串,以下是一些例子:
1 | a="A" |
布尔运算
shell中,用!
表示“非”, -a
表示“与”, -o
表示“或”:
1 | a=10 |
逻辑运算符
逻辑运算符与布尔运算符起着相似的作用,用 &&
表示“与”, ||
表示“或”:,但在表达形式上有不同,此时表达式应使用双层方括号:
1 | a=10 |
字符串测试
在shell中,除了之前的相等、不相等之外,有一些其它的字符串测试运算符,具体如下:
-z
:检测字符串长度是否为0,如果是0,返回true,否则返回false-n
:检测字符串长度是否为0,如果是0,返回false,否则返回true[ ]
:检测字符串是否为空,如果是空,返回false,否则返回true
以下是一些用法示例:
1 | str='' |
流程控制
分支结构
if语句
之前的示例代码中已经用到了分支结构,现在具体讲述。条件分之语句主要结构如下,其中可以没有else
或elif
分支,也可以有多个并列的elif
分支。需要注意的是,分支内的语句不能为空。在分支语句的最后必须有fi
标记结束。
1 | if [ expression1 ] |
在条件语句中,表达式和方括号之间必须有空格,否则会有语法错误。
case语句
与C语言中的switch...case
结构相似,但写法大不相同。shell中的case
语句结构如下:
1 | case 值 in |
以下是一个示例,输入一个数字,根据输入的数字进行判断执行分支语句:
1 | echo 'Input a number between 1 to 4' |
如果多种模式对应同一种相同的处理方式,则可以将模式合并:
1 | while : |
循环结构
for循环
for
循环的结构如下所示,其中“列表”可以是一个数组,或者用空格分隔的数字或字符串:
1 | for 变量 in 列表 |
以下是具体的示例:
1 | arr=(1 2 3) |
while循环
while
循环结构如下:
1 | while condition |
非常简单,以下是一个示例:
1 | int=1 |
另一个例子:
1 | int=1 |
while
可用于构造无限循环,格式如下:
1 | while : |
或
1 | while true |
也可以使用while
配合read
来获取用户输入,以下是一个示例:
1 | echo -n 'please type a number: ' |
until循环
until
循环中,循环结构一直执行,直到条件为真时停止:
1 | until condition |
以下是一个示例:
1 | a=9 |
执行后会输出:
1 | 9 |
break和continue
使用break
可以跳出循环,当有多层循环嵌套时,break
可以跟一个参数用以指定跳出第几层循环,以下是两个break
的使用案例:
1 | for var1 in 1 2 3 |
以上脚本执行输出结果为:
1 | 1 0 |
continue
会跳过本次循环,并继续执行循环,与C语言中相似,不再赘述。
文件测试
以下是shell中文件测试运算符:
运算符 | 描述 |
---|---|
-b file | 检测文件是否是块设备文件,如果是,则返回 true |
-c file | 检测文件是否是字符设备文件,如果是,则返回 true |
-d file | 检测文件是否是目录,如果是,则返回 true |
-f file | 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true |
-g file | 检测文件是否设置了 SGID 位,如果是,则返回 true |
-k file | 检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true |
-p file | 检测文件是否是具名管道,如果是,则返回 true |
-u file | 检测文件是否设置了 SUID 位,如果是,则返回 true |
-r file | 检测文件是否可读,如果是,则返回 true |
-w file | 检测文件是否可写,如果是,则返回 true |
-x file | 检测文件是否可执行,如果是,则返回 true |
-s file | 检测文件是否为空(文件大小是否大于0),不为空返回 true |
-e file | 检测文件(包括目录)是否存在,如果是,则返回 true |
一个例子:
1 | touch ./test.sh |
IO重定向
Unix 命令默认从标准输入设备(stdin)获取输入,将结果输出到标准输出设备(stdout)显示。一般情况下,标准输入设备就是键盘,标准输出设备就是终端,即显示器。
基本操作
使用IO重定向,可以从文件读取输入,或者将输出写到文件中。以下是一些重定向操作:
指令 | 描述 |
---|---|
command > file | 将输出重定向到 file。 |
command < file | 将输入重定向到 file。 |
command >> file | 将输出以追加的方式重定向到 file。 |
n > file | 将文件描述符为n的文件重定向到 file。 |
n >> file | 将文件描述符为n的文件以追加的方式重定向到 file。 |
n >& m | 将输出文件m和n合并。 |
n <& m | 将输入文件m和n合并。 |
<< tag | 将开始标记tag和结束标记tag之间的内容作为输入。 |
注意表中的文件描述符n
,文件描述符0
通常是标准输入(stdin),1
是标准输出(stdout),2
是标准错误输出(stderr)。
以下是一些示例:
1 | echo hello world > file1.txt # 向file1写入 |
此时file1.txt
的内容如下:
1 | hello world |
Here Document
Here Document 是shell中的一种特殊的重定向方式,它的基本的形式如下,用于将两个delimiter
之间的内容传递给command
:
1 | command << delimiter |
如以下的示例代码,会输出3
:
1 | wc -l << deli |
/dev/null 文件
如果希望执行某个命令,但又不希望在屏幕上显示输出结果,那么可以将输出重定向到/dev/null
,/dev/null
是一个特殊的文件,写入到它的内容都会被丢弃;如果尝试从该文件读取内容,那么什么也读不到。将命令的输出重定向到它,会起到”禁止输出”的效果。
例如,如果希望屏蔽 stdout(1)和 stderr(2),可以这样写:
1 | command > /dev/null 2>&1 |
函数
shell中的函数需要先定义后使用,定义函数的方法如下,位于开始位置的function
关键词也可以省略不写:
1 | function function_name () { |
函数的返回值可以使用return
显式指定,也可以略去直接使用最后一行命令执行的结果作为返回值。函数的返回值只能是整数。调用函数时,直接使用函数名即可,不需要加括号。如果向函数传递参数,直接在函数后边跟上用空格分隔的参数列表,函数内部使用$n
的方式获取,类似于脚本执行参数。在调用函数之后,获取函数的返回值应当使用#?
。
以下是两段示例代码:
1 | fun1(){ |
执行上边的脚本会得到以下输出:
1 | this is fun1 |
再来另一个示例:
1 | fun2(){ |
以上脚本执行结果如下:
1 | 1st arg 10 is less than 2nd arg 25 |
文件包含
和其他语言一样,shell也可以包含外部脚本。这样可以很方便的封装一些公用的代码作为一个独立的文件。包含文件格式如下:
1 | 第一种形式 注意点号(.)和文件名中间有一空格 |
以下是一个简单示例:
首先准备文件file1.sh
,里边输入内容如下,此时file1.sh
不需要可执行权限:
1 | !/bin/bash |
接下来在新的脚本中输入以下内容:
1 | . ./file1.sh |
执行之后会有以下输出结果:
1 | var1 from file1 |