Shell Study


本文记录一些自己在使用shell进行批量操作、任务调度等工作时用到的一些shell的基础知识,在此记录以备翻阅和查找。

基本使用

变量的定义和使用

变量的定义

在定义变量时,变量名不加美元符号,例如:

1
val_name="this is value"

需要注意的是,变量名和等号之间不能有空格。同时,变量名的命名遵循如下规则:

首个字符必须是字母(a-z A-Z)。
中间不能有空格,可以使用下划线。
不能使用标点符号。
不能使用bash中的关键字。
除了显式的直接赋值,还可以使用语句给变量赋值,如:

1
for file in `ls /etc`

使用变量

使用一个定义过的变量,只要在变量名前面加美元符号,例如:

1
2
echo $val_name
echo ${val_name}

上面的花括号是可选的,为了帮助解释器识别变量的边界,如:

1
echo "this is my ${val_name}value"

注意,重复使用的变量赋值时不可以使用$符号。

只读变量

使用readonly命令定义可以将变量定义为只读变量,只读变量的值不能被改变。

1
2
3
baidu_uri="baidu.com"
readonly baidu_uri
baidu_uri="google.com"

这样的执行就会报错“/bin/sh: NAME: This variable is read only.”。

删除变量

使用unset命令可以删除变量,但是不能删除只读变量:

1
2
3
#!/bin/sh
myUrl="http://www.baimoon.com"
unset myUrl

变量类型

运行shell的时候,会同时存在三种变量:

局部变量:局部变量在脚本或命令中定义,仅在当前shell实例中有效,其他shell启动的程序不能访问局部变量。
环境变量:所有程序,包括shell启动的程序,都能访问环境变量,有些程序需要环境变量来保证其正常运行。必要的时候,shel也可以定义环境变量。
shell变量:shell变量是由shell程序设置的特殊变量。shell变量中有一部分是环境变量,有一部分是局部变量,这些变量保证了shell的正常运行。

shell字符串

字符串是shell编程中最常用最有用的数据类型,字符串可以用单引号,也可以使用双引号,也可以不用引号。
使用单引号的限制:

单引号里的任何字符都会原样输出,单引号中的变量是无效的。
单引号字符串中不能出现单引号(对单引号使用转义字符也不行)。

双引号的优点:

双引号里可以有变量。
双引号里可以出现转义字符。

拼接字符串

1
2
3
4
your_name="qinjx"
greeting="hello, "$your_name" !"
greeting_1="hello, ${your_name} !"
echo $greeting $greeting_1

获取字符串长度

1
2
string="abcd"
echo ${#string} #输出 4

提取子字符串

1
2
string="runoob is a great site"
echo ${string:1:4} # 输出 unoo

查找子字符串

1
2
string="runoob is a great company"
echo `expr index "$string" is` # 输出 8

Shell数组

bash支持一维数组(不支持多维数组),并且没有限定数组的大小。数组的下标由0开始,获取数组的元素需要使用下标,下标可以是整数或算数表达式,算数表达式的值应该大于等于0。

数组定义

在shell中,用括号来表示数组,数组元素使用空格符号来分割。定义数组的一般形式为:

1
数组名=(值1 值2 ... 值n)

例如:

1
array_name=(value0 value1 value2 value3)


1
2
3
4
5
6
array_name=(
value0
value1
value2
value3
)

还可以单独定义数组的各个分量:

1
2
3
array_name[0]=value0
array_name[1]=value1
array_name[n]=valuen

可以不使用连续的下标,而且下标的范围没有限制。

读取数组

读取数组需要使用下标,一般格式为:

1
${数组名[下标]}

使用@符号可以获取数组中的所有元素:

1
echo ${array_name[@]}

获取数组的长度

获取数组长度的方法与获取字符串长度的方法相同:

1
2
3
4
5
6
# 取得数组元素的个数
length=${#array_name[@]}
# 或者
length=${#array_name[*]}
# 取得数组单个元素的长度
lengthn=${#array_name[n]}

Shell的注释

以”#”开头的行就是注释,会被解释器忽略。
shell中没有多行注释,只能每行添加一个”#”。

Shell传递参数

我们可以在执行shell脚本时,向脚本传递参数。脚本获取参数的格式为$n,其中n代表一个数字。参数的下标是从1开始的,0表示执行的文件名。

1
2
3
4
5
6
7
8
9
#!/bin/bash
# author:菜鸟教程
# url:www.runoob.com
echo "Shell 传递参数实例!";
echo "执行的文件名:$0";
echo "第一个参数为:$1";
echo "第二个参数为:$2";
echo "第三个参数为:$3";

其他参数:

参数表示 参数说明
$# 传递到脚本的参数的个数
$* 以一个字符串显示所有向脚本传递的所有参数
$$ 运行脚本的当前进程的ID
$! 后台运行的最后一个进程的ID号
$@ 与$*相同,但是使用时加引号,并在引号中返回每个参数
$- 显示Shell使用的当前选项,与set命令功能相同
$? 显示命令的退出状态。0表示没有错误,其他值表示有错误

$*与$@的区别

相同点:都是引用所有参数。
不同点:只有在双引号中体现出来。假设在脚本运行时写了三个参数1 2 3,则$*等价于”1 2 3”(一个参数),而$@等价于”1” “2” “3”(三个参数)。

Shell基本运算符

shell支持的基本运算符有:

算数运算符
关系运算符
布尔运算符
字符串运算符
文件测试运算符

原生的bash不支持简单的数学运算,但是可以通过其他命令来实现,例如awk和expr。其中expr比较常用。expr是一款表达式计算工具,使用它能够完成表达式的求值操作。需要注意的两点:

表达式和运算符之间必须有空格。
完整的表达式要被``包含起来。

算数运算符

运算符 说明 举例
+ 加法 expr $a + $b就是在计算a与b的和
- 减法 expr $a - $b就是在计算a与b的查
* 乘法 expr $a \* $b就是在计算a与b的乘积
/ 除法 expr $a / $b就是在计算a与b的商
% 取余 expr $a % $b就是在计算a与b的余
= 赋值 a=$b,将b的值赋值给a
== 相等,比较左右两边是否相当 [ $a == $b ]比较a和b是否相当
!= 不想当,比较左右两边是否不相等 [ $a != $b ]

注意,

条件表达式要放在放括号之间,并且要有空格。
乘号()前面必须加反斜杠才能实现乘法运算。
在MAC中,shell的expr语法是$(()),此处表达式中的
不需要反斜杠来转意。

关系运算符

关系运算符只支持数字,不支持字符串,除非字符串的值是数字。

运算符 说明 举例
-eq 检测两个数是否相等,相等返回true [ $a -eq $b ]
-ne 检测两个数是否相等,不相等返回true [ $a -ne $b ]
-gt 检测左边的数是否大于右边的数,大于返回true [ $a -gt $b ]
-lt 检测左边的数是否小于右边的数,小于返回true [ $a -lt $b ]
-ge 检测左边的数是否大于等于右边的数,大于等于则返回true [ $a -ge $b ]
-le 检测左边的数是否小于等于右边的数,小于等于则返回true [ $a -le $b ]

布尔运算符

运算符 说明 举例
! 非运算 [ !false ] 返回true
-o 或运算 [ $a -lt 20 -o $b -gt 30 ]
-a 与运算 [ $a -lt 20 -a $b -gt 30 ]
&& 逻辑与运算符 [[ $a -lt 100 && $b -lt 20 ]]
逻辑或运算符 [[ $a -lt 100 $b -lt 20 ]]

字符串运算符

运算符 说明 举例
= 检测两个字符串是否相等,相等返回true [ $a = $b ]
!= 检测两个字符串是否相等,不相等返回true [ $a != $b ]
-z 检测字符串长度是否为0,为0返回true [ -z $a ]
-n 检测字符串长度是否为0,不为0返回true [ -n $a ]
str 检测字符串是否为空,不为空返回true [ $a ]

文件测试运算符

操作符 说明 举例
-b file 检测文件是否是二进制文件,是则返回true [ -b $file ]
-c file 检测文件是否是字符文件,是则返回ture [ -b $file ]
-d file 检测文件是否是目录,是则返回true [ -d $file ]
-f file 检测文件是否是普通文件(既不是目录,也不是设备文件) [ -f $file ]
-g file 检测文件是否设置了SGID位,是则返回true [ -g $file ]
-k file 检测文件是否设置了粘着位(Sticky Bit),是则返回true [ -k $file ]
-p file 检测文件是否是有名管道,是则返回true [ -p $file ]
-u file 检测文件是否设置了 SUID 位,是则返回true [ -u $file ]
-r file 检测文件是否可读,是则返回true [ -r $file ]
-w file 检测文件是否可写,是则返回true [ -w $file ]
-x file 检测文件是否可执行,是则返回true [ -x $file ]
-s file 检测文件是否为空,不为空则返回true [ -s $file ]
-e file 检测文件是否存在,存在则返回true [ -e $file ]

Shell echo命令

Shell的echo命令用于字符串的输出。命令格式:

1
echo string

可以使用echo实现更加复杂的输出格式控制。

显示普通字符串

显示普通的字符串,如:

1
echo "It is a test"

这里的双引号完全可以省略:

1
echo It is a test

显示变量

1
2
echo "$name It is a test"
echo "${name} It is a test"

显示换行

1
echo -e "OK! \n"

显示结果定向到文件

1
echo "It is a test" > myfile

原样输出字符串,不进行转义或取变量(用单引号)

1
echo '$name\"'

显示命令执行结果

1
echo `date`

Shell printf命令

printf是标准所定义,因此使用它比使用echo移植性好。printf使用引用文本或空格分隔的参数,外面可以在printf中使用格式化字符串,还可以定制字符串的宽度、左右对齐方式等。默认printf不会像echo那样自动添加换行,需要手动添加\n。
printf语法:

1
printf format-string [arguments ... ]

其中 format-string为格式控制字符串 ;arguments为参数列表。
示例:

1
2
3
4
5
6
#!/bin/bash
printf "%-10s %-8s %-4s\n" 姓名 性别 体重kg
printf "%-10s %-8s %-4.2f\n" 郭靖 男 66.1234
printf "%-10s %-8s %-4.2f\n" 杨过 男 48.6543
printf "%-10s %-8s %-4.2f\n" 郭芙 女 47.9876

%s %c %d %f都是格式替代符。%-10s 指一个宽度为10个字符(-表示左对齐,没有则表示右对齐),任何字符都会被显示在10个字符宽的字符内,如果不足则自动以空格填充,超过也会将内容全部显示出来。
%-4.2f 指格式化为小数,其中.2指保留2位小数。

printf的转义序列

序列 说明
\a 警告字符,通常为ASCII的BEL字符
\b 后退
\c 抑制(不显示)输出结果中任何结尾的换行符(只在%b格式指示符控制下的参数字符串有效),而且任何留在参数里的字符、任何接下来的参数以及任何留在格式字符串中的字符都会被忽略
\f 换页
\n 换行
\r 回车
\t 水平制表符
\v 垂直制表符
\ 转义的饭斜杠
\ddd 表示1到3位八进制的字符。仅在格式字符串中有效
\0ddd 表示1到3位的八进制字符

Shell test命令

test命令用于检测某个条件是否成立,他可以进行数值、字符和文件三个方面的测试:

数值测试

参数 说明
-eq 是否等于
-ne 是否不等于
-gt 是否大于
-ge 是否大于等于
-lt 是否小于
-le 是否小于等于

示例:

1
2
3
4
5
6
if test ${num1} -eq ${num2}
then
echo "eq"
else
echo "not eq"
fi

字符串测试

字符串测试可用的操作符,详见”Shell基本运算符”章节。
示例:

1
2
3
4
5
6
if test $str1 = $str2
then
echo "eq"
else
echo "not eq"
fi

文件测试

文件测试可用的操作符,详见”Shell基本运算符”章节。
示例:

1
2
3
4
5
6
if test -e ./notFile -o -e ./bash
then
echo 'samething is here'
else
echo 'nothing is here'
fi

Shell 流控制

和其他语言的流控制不同,shell的流控制语句不能为空,如果为空就不要写。

if else

if语句的语法格式

1
2
3
4
if condition
then
command
fi

如果写成一行:

1
if [ condition ]; then command; fi

if else语句的语法格式

1
2
3
4
5
6
7
if condition
then
command1
command2
else
command
fi

if else-if else语句的语法格式

1
2
3
4
5
6
7
8
9
if condition
then
command1
elif
then
command2
else
command3
fi

if else语句经常与test命令结合使用

1
2
3
4
5
6
if test ${num1} -eq ${num2}
then
echo "eq"
else
echo "not eq"
fi

for循环

for循环与其他编程语言类似,一般格式为:

1
2
3
4
for var in item1 item2 itemN
do
command1
done

如果写成一行:

1
for var in item1 item2 itemN; do command; command2; commandN done;

循环体中的命令可以是任何有效的shell和语句。in列表是可选的,如果不使用它,for循环使用命令行的位置参数。
无限循环:

1
for(( ; ; ))

while循环

while循环用于不断的执行一系列命令,也用于从文件中读取数据。其格式为:

1
2
3
4
while condition
do
command
done

无限循环:

1
2
3
4
while :
do
command
done

until循环

until循环执行一系列命令,直到条件为true时停止。until循环与while循环在处理方式上刚好相反。一般while循环由于until循环,但在某些情况下,until循环更加有用。
until语法格式:

1
2
3
4
until condition
do
command
done

条件可以为任意测试条件,测试发生在循环末尾,因此循环至少执行一次。

case语句

case语句为多选择语句。可以用case语句匹配一个值与一个模式,如果匹配成功,执行匹配的命令。case语句格式如下:

1
2
3
4
5
6
7
8
9
10
case value in
模式1)
command1
;;
模式2)
command2
;;
*)
;;
esac

case工作方式如上所示。取值后面必须为单词in,每一种模式必须以右括号结束。取值可以为变量或常数。匹配发现取值符合某一模式后,其间所有命令开始执行至;;。
取值将检测匹配的每一个模式。一旦模式匹配,则执行完全匹配模式响应命令后不再继续匹配其他模式。如果无一匹配模式,使用星号捕获,再执行后面的命令。
case的语法和传统的编程语言不太一样,它需要一个esac来结束case语句,每个case分值用右括号来表示,并用两个分号表示break。

跳出循环

在执行循环的过程中,有时候需要在某种特定情况下终止循环,shell中使用两个命令来实现:continue和break。

break语句

break语句用来跳出并结束循环(后面的循环都执行了)。

1
2
3
4
5
6
7
while :
do
command
if test $a -q 1
break
fi
done

continue语句

continue语句也是用来跳出循环,但是只是跳出当前循环,只是continue语句后面的不执行了,但是下一个循环继续。

1
2
3
4
5
6
7
8
9
10
11
12
while :
do
read aNum
case $aNum in
1|2|3)
echo '1'
;;
*)
continue
;;
esac
done

Shell文件的引入

shell脚本也可以引入其他的shell脚本,这样可以很方便利用已有代码来提高效率。
shell文件引入的语法如下:

1
2
3
. filename
source filename

注意,被引入的文件不需要可执行权限。

高级使用

Shell的函数

shell可以允许用户定义函数,然后在shell中随便调用。
shell中函数的定义格式如下:

1
2
3
4
5
[ function ] funcName [ () ]
{
action;
[return int;]
}

说明:

定义函数时,关键字function可以省略;而且可以不带任何参数。
返回参数,可以显示的使用return语句,如果没有return语句,则最后一条命令的运行结果作为返回值。return后跟数值n(0-255)

示例:

1
2
3
4
5
6
7
8
#! /bin/bash
demoFun() {
echo 'this is a demo'
return 1
}
demoFun

函数的返回值在调用函数后,通过$?来获取。
另外,函数在使用前,必须定义。这表示,函数的调用位置必须在函数的定义位置之后。而且函数的调用,只使用函数名即可。

函数的参数

调用函数时可以向其传递参数。在函数内部,通过$n的形式来获取参数的值,例如$1表示第一个参数,$2表示第二个参数。当参数超过10个时,从第10个开始,需要使用${N}的方式来获取。
示例:

1
2
3
4
5
6
7
8
funWithArg()
{
echo $1
echo ${10}
return $3
}
funWithArg 1 2 3 4 5 6 7 8 9 10

另外,还有几个特殊的字符来表示参数
| 参数 | 说明 |
|:————–|:———————|
| $# | 传递到函数中的参数的个数 |
| $ | 以一个字符串来显示所有传入到函数中的参数 |
| $$ | 脚本运行的当前进程ID |
| $! | 后台运行的最后一个进程ID |
| $@ | 与$
相同,只是每个参数以带引号的方式返回 |
| $- | 显示shell使用的当前选项,与set命令功能相同 |
| $? | 表示函数的返回值 |

时间的使用

在执行shell调度的时候,难免需要操作时间,来生成特定的时间格式。

date命令

date命令的功能是显示和设置系统的日期和时间。date命令中的各个选项的含义分别为:

-d, –date 提供日期字符串作为输入
-s, –set 设置日期格式
-u, –universal 显示或设置通用时间

日期格式字符串列表

注意使用这些符号时,加号和格式符号之间不能有空格,如:date +”%H”。

格式字符串 表示含义
%H 小时,24小时制(00 - 23)
%I 小时,12小时制(01 - 12)
%k 小时,24小时制(1 - 24)
%l 小时,12小时制(1 - 12)
%M 分钟(01 - 59)
%p 显示AM或PM
%r 显示时间,以12小时制显示(hh:mm:ss %p)
%s 从1970年1月1日 00:00:00到目前经历的秒数
%S 显示秒(00-59)
%T 显示时间,以24小时制显示(hh:mm:ss)
%Z 显示时区,日期域(CST)
%X 显示时间的格式(%H:%M:%S),如:14时37分21秒
%x 显示日期的格式(%Y/%m/%d)
%a 星期的简称(Sun - Sat)
%A 星期的全称(Sunday - Staturday)
%h, %b 月的简称(Jan - Dec)
%B 月的全称(January - December)
%c 日期和时间(Tue Nov 20 14:12:48 2017)
%d 当日在当月中的天数(01 - 31)
%D 日期(mm/dd/yy)
%x, %D 日期(mm/dd/yy)
%j 一年的第几天(001 - 366)
%m 月份(01 - 12)
%w 当天是本周的第几天(0代表星期天)
%W 本周是一年的第几个星期(00 - 53, 星期一为第一天)
%y 年的最后两位数字(17)
%Y 年的完整表示(2017)

用例

以24小时制输出当前时间

1
2
$ date +"%x %T"
2017/03/03 14:29:14

显示当日的月份,是今年第几周的星期几

1
2
$ date +"%B %W %w"
三月 09 5

时间的加减

时间的加减使用-d参数,配合描述操作来完成的,例如:

1
2
3
4
date +"%Y%m%d"
d=`date -d +1 day +%D`
echo $d

其中”+1 day”就是描述操作,类似的还有”+1month”、”+1year”、”+1week”、”+1minute”和”+1second”等,其中减操作,使用”-“。
除了上面的计算,如果两个时间需要计算时间差,那么需要使用以一种变通的方式来解决,将时间转换为秒,然后在计算对应的时间差:

1
2
3
last_year=`date -d "2016-01-01 00:00:00" +"%s"`
this_year=`date +%s`
echo "两个时间差"$(((${this_year}-${last_year}) / 86400))"天"

休眠sleep

在写一些以循环方式运行某些监控脚本时,设置间隔时间是必不可少的,那么就会用到休眠功能:

1
2
3
4
5
#!/bin/bash
for ((i=0;$i<=100;i++))
do
sleep 0.1
done

命令的连续执行

某些时候,我们可能需要执行某些服务的某些脚本,而这些脚本需要再次输入命令。比如ZooKeeper的zkCli.sh脚本,当执行了这个脚本后,会打开Jconsole,需要再次输入要执行的命令。如果在这种情况下,我们如何通过shell连接服务并执行命令呢?操作如下:

1
2
3
4
5
#!/bin/bash
exec "zookeeper/bin/zkCli.sh" -server ${host}:${port} << EOF
addauth digest ${user}:${password}
create ${path} ${data} ${acl}
EOF

对于连续的shell,我们还可以使用&&进行连接,比如 ls /home && rm -f xxx.log

1
2
#!/bin/bash
ls /home && rm -f xxx.log