title: Shell脚本基础篇(二)
tags:
  - Bash
categories:
  - Linux
top_img: false
cover: '/upload/cdn0files/20200721122539.png'
abbrlink: 931fc2ad
date: 2020-01-22 15:30:07
updated: 2020-01-22 15:30:07

上一节我们讲到变量的系统变量

变量

用户变量

用户变量可以由任何字母、数字或者下划线组成的字符串,长度不超过20个字符,且区分大小写。

赋值时变量、等号、值之间不允许出现空格。shell脚本会自动决定变量值的数据类型。

变量每次被引用时,都会输出当前赋给它的值且赋值时不需要美元符号来引用,例如:

#!/bin/bash
var1=10
var2=$var1
echo the true = $var2

那么输出结果就是:

the true = 10

如果不加美元符号,赋值语句变成了var2=var1,那么输出结果会变成:

the true = var1

命令替换

Shell脚本中的特性:从命令输出中提取信息,并赋值给变量。有两种方法:

  • 反引号`` `
  • $() 格式

反引号的用法就是把命令用两个反引号包裹起来,很麻烦,不好示例,有兴趣自己去研究一下,我们主要以$() 方法为例。

例如:test=$(date) 会把date命令的输出结果赋值给test变量

再利用echo语句就可以输出结果啦:echo "the date is $test"

示例:通过命令替换获得当前日期并用来生成唯一文件名

#!/bin/bash
today=$(date +%y%m%d)
ls /usr/bin -al > log.$today

today变量是格式化后的date命令输出结果,这是提取日期信息生成日志文件名常用的方法,+%y%m%d是告诉date命令将日期显示为两位数的年月日组合。

$ date +%y%m%d
200123

命令替换会创建一个子shell来运行对应的命令,subshell是由运行该脚本的shell所创建出来的独立的子shell,所以该子shell所执行命令是无法使用脚本所创建的变量的。

重新定向输入和输出

输出重定向

最基本的重定向是将命令的输出发送到一个文件中,BashShell中用大于号> 来实现这个功能。例如:

$ date > test1.txt

重定向操作创建了一个新文件test1.txt,文件权限由umask 决定,并将date 命令的输出结果写入test1.txt文件中,如果这个文件已经存在并有内容,那么重定向操作会覆盖这个文件的内容。

如果不想覆盖原文件的内容,只是追加写入该文件,可以使用两个大于号>> 来追加写入。例如:

$ date > test.txt
$ who >> test.txt

这样test.txt文件中就会有两条内容,分别是两个命令的输出内容。

输入重定向

输入重定向和输出正好相反,输入重定向的功能是把文件中的内容重定到命令,符号是小于号<

简单的记忆方法就是,在命令行里,命令总是在左边,而重定向符号是“指向”数据的流向。

举一个例子:

$ date > test
$ wc < test
	1	6	29

输入重定向

wc 命令可以对数据中的文本进行计数,默认情况下,它会输出三个数值:

  • 文本的行数
  • 文本的词数
  • 文本的字节数

内联输入重定向

这种方法无需使用文件进行重定向,只需要在命令行中指定用于输入重定向的数据即可,符号为<<

在命令行中使用内联输入重定向时,shell会使用PS2 环境变量中的次提示符来提示输入数据,例如:

内联

次提示符会一直进行提示输入数据,直到输入了作为文本标记的那个字符串。

管道

有时候需要把一个命令的输出作为另一个命令的输入,虽然可以使用重定向来实现但是很麻烦,所以有了管道连接

管道符号是一条单竖线| ,连接两个命令,例如:

cmd1 | cmd2

但是这两个命令并不是依次执行的,Linux实际上会同时运行这两个命令,在系统内部将它们联系起来,在第一个命令产生输出的同时,输出会被立即送给第二个命令,数据传输并不会用到任何中间文件和缓冲区。

比如使用ls -al 命令时输出结果太长,屏幕无法显示完全,我们可以使用more 命令进行分页显示:

ls -al | more

执行数学运算

在shell脚本中有两种方式可以执行数学运算:

expr

例如:

expr

但是expr 命令只能识别少数的数学和字符串操作符:

操作符描述
ARG1 | ARG2如果ARG1既不是null 也不是零值,返回ARG1,否则返回ARG2
ARG1 & ARG2如果没有参数是null 或零值,返回ARG1,否则返回0
ARG1 < ARG2小于返回1,否则返回0
ARG1 <= ARG2小于等于返回1,否则返回0
ARG1 = ARG2等于返回1,否则返回0
ARG1 != ARG2不等于返回1,否则返回0
ARG1 >= ARG2大于等于返回1,否则返回0
ARG1 > ARG2大于返回1,否则返回0
ARG1 + ARG2运算和
ARG1 - ARG2运算差
ARG1 * ARG2乘积
ARG1 / ARG2算术商
ARG1 % ARG2算术余数
STRING : REGEXP如果REGEXP匹配到了STRING中的某个模式,返回该模式匹配
match STRING REGEXP如果REGEXP匹配到了STRING中的某个模式,返回该模式匹配
substr STRING POS LENGTH返回起始位置为POS(从1开始计数)、长度为LANGTH个字符的子字符串
index STRING CHARS返回在SREING中找到CHARS字符串的位置,否则返回0
length STRING返回字符串STRING的数值长度
+ TOKEN将TOKEN解释成字符串,即使是个关键字
(EXPRESSION)返回EXPRESSION的值

但是非常严重的一点就是很多操作符在shell中是有特殊含义的,比如星号* ,如果进行乘法运算就会有诡异的结果:

报错

要让expr 命令正确执行运算就要用到转义符\ :

expr 1 \* 2

那么就可以在shell脚本中使用数学计算了:

#!/bin/bash
var1=10
var2=20
var3=$(expr $var2 / $var1)
echo result is $var3

结果自然是:result is 2

使用方括号

在bash中将一个数学运算结果赋给某个变量时,可以用方括号将表达式括起来:$[1 + 1]

例如:

#!/bin/bash
var1=100
var2=50
var3=45
var4=$[$var1 * ($var2 - $var3)]
echo "result is $var4" 

输出结果为:result is 500

在方括号内不用担心转义,shell知道那是运算符而不是通配符。

bash shell只支持整数运算,而zsh shell支持完整的浮点数算术操作

浮点解决方案

bc基本用法

bash计算器实际上是一种编程语言,它允许在命令行中输入浮点表达式,然后解释计算,bash计算器能够识别:

  • 数字(整数和浮点数)
  • 变量(简单变量和数组)
  • 注释(以#或者/* */开头的行)
  • 表达式
  • 编程语句(例如if then语句)
  • 函数

bash shell环境下可以通过bc 命令访问bash计算器:

bc

现在就可以通过在脚本中使用bc来进行运算了:

#!/bin/bash
var1=10.46
var2=43.67
var3=33.2
var4=71

var5=$(bc << EOF
	scale = 4
	al = ($var1 * $var2)
	bl = ($var3 * $var4)
	al + bl
	EOF
	)

echo "result is $var5"

我们在bc计算器中创建了变量albl 等并且可以进行赋值,但是在bc计算器中的变量只能在计算器中使用,不能在shell脚本中使用。

退出脚本

shell脚本中的每个命令都使用退出状态码告诉shell它已经运行完毕,退出状态码是一个0~255之间的整数值,在命令结束时由命令传给shell。

查看退出状态码

Linux提供了一个专门的变量$? 来保存上一个已执行命令的退出状态码。

退出状态码

默认情况下,如果一个命令成功执行,那么退出状态码就是0,反之,命令执行错误那它的退出状态码就是一个正常数值:

错误状态码

虽然Linux的错误码没有规律,但是默认情况下会有一个参考:

状态码描述
0命令成功结束
1一般性未知错误
2不适合的shell命令
126命令不可执行
127没找到命令
128无效的退出参数
128+x与Linux系统信号x相关的严重错误
130通过Ctrl + C 结束的命令
255正常范围之外的退出状态码

exit

默认情况下,shell脚本会以脚本中最后一个执行的命令的退出状态码退出。

exit 命令可以让你自己指定一个退出状态码,甚至可以是变量。

#!/bin/bash
var1=10
var2=20
var3=$(expr $var2 / $var1)
echo result is $var3
exit $var3

但是有一点要注意,退出状态码的最大值为255,如果超过这个数值,shell就会通过模运算得到最终的退出状态码值,例如指定状态码为300,shell会拿它除以256,余数是44,那么最终的退出状态码就是44:

44

结构化命令

使用if then语句

语法如下:

if command
then
	command
fi

if后面那个命令的退出状态码就是if 的判断条件,如果命令成功执行状态码为0,if语句继续执行,如果命令的状态码是其他值,那么then 部分的命令就不会执行,fi 表示语句结束。

举一个简单的例子:

#!/bin/bash
if pwd
then
	echo "test"
fi

pwd命令成功执行,结果自然就是输出test文本,举一个反例:

#!/bin/bsah
if notcommand
then
	echo "test"
fi
echo "not test"

notcommnd 就不是一个shell的命令,所以then 里面的命令并不会执行,脚本执行的输出自然是not test。

当然if then还有另外一种写法:

if command; then
	command
fi

用法是一样的啦,不过把then放在if的同一行了。

if then else语句

语法:

if command
then
	command
else
	command
fi

举个例子:

#!/bin/bash
testusr=laugh
if grep $testusr /etc/passwd
then
	echo "The bash files for usr $testusr are:"
	ls -a /home/$testusr/.b*
	echo
else
	echo "The usr $testusr does not exist on this system."
	echo
fi

上述例子中,如果testusr 的值是系统中存在的用户名,则正常输出家目录下的bash信息,如果不存在则输出一句话告诉这个用户不存在。

嵌套if

还是用例子来说明吧:

#!/bin/bash
testusr=NoUser
if grep $testusr /etc/passwd
then
	echo "the usr $testusr exists on this system"
else
	echo "the usr $testusr does not exist on this system"
	if ls -d /home/$testusr/
	then
		echo "however $testusr have a directory"
	fi
fi

检测是否存在这个用户,存在输出存在不存在输出不存在并且查看家目录下是否有这个用户的家目录。

嵌套if还有另外一个形式:

if command
then
	command
elif command2
then
	commands
fi

喜欢用那种看个人。

再来看个例子:

#!/bin/bash
testusr=NoUsr
if grep $testusr /etc/passwd
then
	echo "the usr $testusr exists on this system"
elif ls -d /home/$testusr
then
	echo "the usr $testusr does not exist on this system"
	echo "however $testusr has a directory"
else
	echo "the usr $testusr does not exist on this system"
	echo "and $testusr does not have a directory"
fi

当然使用更多的嵌套也是可以的,但是上面这个例子要注意的是else块是属于elif块的而不是if-then块的。

test命令

if-then语句有一个致命的缺点就是不能判断退出状态码之外的条件,test 命令提供了在if-then语句中测试不同条件的途径。如果test 命令中的条件成立,命令就会退出并返回退出状态码0,反之则是其他。如果test命令的参数部分为空,也会以非零的状态码退出。例如:

#!/bin/bash
testnr="FULL"
if test $testnr
then
	echo "true"
else
	echo "false"
fi

就上述例子而言,输出结果就是true 如果变量testnr 的值是空的testur=""那么输出结果就是false

方括号判断

还有一种更方便的判断方法:

if [ command ]
then
	commands
fi

用法和test命令相同,但是要特别注意空格哦。

test命令可以判断三种条件:

  • 数值比较
  • 字符串比较
  • 文件比较

数值比较

常见参数:

语句描述
n1 -eq n2检查n1是否等于n2
n1 -ge n2检查n1是否大于等于n2
n1 -gt n2检查n1是否大于n2
n1 -le n2检查n1是否小于等于n2
n1 -lt n2检查n1是否小于n2
n1 -ne n2检查n1是否不等于n2

举个例子:

var1=10
var2=20
if [ $var1 -lt $var2 ]
then
	echo "true"
else
	echo "false"
fi

输出结果自然是true,反之把var1var2的值换一下输出结果则是flase

注意test 命令和方括号都不支持浮点数的比较。只能使用整数

字符串比较

语句描述
str1 = str2检查str1是否与str2相同
str1 != str2检查str1是否与str2不同
str1 < str2检查str1是否比str2小
str1 > str2检查str1是否比str2大
-n str检查str的长度是否非零
-z str检查str的长度是否为零

比较字符串值相等就不用说了,麻烦的是比较字符串的大小,因为shell会把大于小于号识别为重定向符号,举一个反例:

反例

可以看到脚本报错并创建了一个新文件,且输出了false 是由于shell把大于号判定为重定向操作符,此时只需要转义即可:

#!/bin/bash
var1=hahaha
var2=ha
if [ $var1 \> $var2 ]
then
	echo "true"
else
	echo "false"
fi

这样就能正确判断并输出啦。

还有一种困扰,如果当两个字符串长度相等只是大小写不同怎么进行比较,test命令使用标准的ASCII顺序,根据每个字符的ASCII数值来决定排序结果,所以会判定大写字母小于小写字母,来看个例子:

#!/bin/bash
var1=test
var2=Test
if [ $var1 \> $var2 ]
then
	echo "true"
else
	echo "false"
fi

输出结果:

结果

字符串大小比较参数-n、-z 没啥说的,要注意的是-z即使这个变量没有被定义它也会成功执行,判断为变量长度为零。

文件比较

文件比较可以测试系统上文件和目录的状态。

参数描述
-d file检查file是否存在并是一个目录
-e检查file是否存在
-f检查file是否存在并是一个文件
-r检查file是否存在并可读
-s检查file是否存在并非空
-w检查file是否存在并可写
-x检查file是否存在并可执行
-O检查file是否存在并属当前用户所有
-G检查file是否存在并默认组与当前用户相同
file1 -nt file2检查file1是否比file2新
file1 -ot file2检查file是否比file2旧

直接上例子:

#!/bin/bash
testdir=/home/test
if [ -d $testdir ]
then
	echo "the $testdir directory exists"
	cd $testdir
	ls -al
else
	echo "the $testdir directory does not exist"
fi

检测$testdir变量中的文件夹是否存在,如果存在则切换到该工作目录并输出所有文件信息,如果不存在则输出不存在。

#!/bin/bash
location=$HOME/test
filename=test.sh
if [ -e $location ]
then
	echo "the dir is true"
	echo "now check on the file $filename"
	if [ -e $HOME/test/$filename ]
	then
		echo "the file is true"
		ls -l /$HOME/test/$filename
	else
		echo "the file is false"
	fi
else
	echo "the dir is false"
fi

先检查location的目录是否存在,若存在则输出文件夹true并检查该目录下是否有filename 这个文件,如果有则输出文件true并显示文件详细信息如果没有则显示文件false,如果文件夹不存在则输出文件夹false:

执行结果

-e 这个参数可用于文件或者目录是否存在,但是要确定的指定对象为文件,必须用-f 参数,确定为目录就要用-d

#!/bin/bash
filename=$HOME/test
echo
echo "now i will check the file $filename"
echo
#检查该文件是否存在
if [ -e $filename ]
then
	echo "the $filename is true"
	echo "but it is a file ?"
	#检查是否是一个文件
	if [ -f $filename ]
	then
		echo "yes it is a file" #是文件并输出相信信息
		ls -l $filename
	else
		echo "no it is not a file" #不是一个文件
		echo "but it is a dir ?"
		#检查是否是一个文件夹
		if [ -d $filename ]
		then
			echo "yes it is a dir"
		else
			echo "i do not know"
		fi
	fi
else
	echo "the $filename is false"
fi

除了检查文件存在还能检查文件的权限等,用法和-d、-e 差不多就不赘述了,还有检查文件的日期,比较两个文件的更改时间等。

复合条件测试

if-then语句可以使用布尔逻辑运算符,语法如下:

[ command1 ] && [ command2 ]
[ command1 ] || [ command2 ]

第一种运算符是AND运算,两个条件必须同时满足才可以返回退出状态码0,第二种是OR运算,两个条件中有一个条件复合即可。例如:

#!/bin/bash
if [ -d $HOME/test ] && [ -d $HOME/test ]
then
	echo "dir is true and you can write it"
else
	echo "i do not know"
fi

如果test不是一个目录或者没有可写的权限就会输出idonotknow

if-then的高级特性

  • 用于数学表达式的双括号
  • 用于高级字符串处理功能的双方括号

双括号

test 命令只能进行简单的数学运算,但是双括号允许在bash中使用更高级的数学表达式。

符号描述
val++后增
val--后减
++val先增
--val先减
!逻辑求反
~位求反
**幂运算
<<左位移
>>右位移
&位布尔和
|位布尔或
&&逻辑和
||逻辑减

例如:

#!/bin/bash
var1=10
if (( $var1 ** 2 > 90 ))
then
	(( var2 = $var1 ** 2 ))
	echo "the square of $var1 is $var2"
fi

输出结果

双方括号

双方括号命令提供了针对字符串比较的高级特性,它采用了test 命令中的标准字符串比较,而且支持一个test 命令中所没有的新特性:模式匹配

注意,双方括号在bash shell中工作良好,但是其他shell支持不支持我就不知道了。

在模式匹配中可以自定义一个正则表达式来匹配字符串:

#!/bin/bash
if [[ $USER == l* ]]
then
	echo "hello $USER"
else
	echo "i do not know"
fi

正则表达式之后再讲。

case命令

如果要在一组值中匹配特定的值,但是如果写if-then-else 语句的话会很长很长,用case 命令就会简单很多,例如:

#!/bin/bash
case $USER in
laugh | laugh-ubuntu )
	echo "welcome $USER"
	echo "hello" ;;
testing )
	echo "do not forget to log off when you are done" ;;
testing2 )
	echo "hello world" ;;
* )
	echo "hahahah" ;;
esac

注意分号和小括号哦

for命令

for 命令可以循环执行定义好的一组命令,基本格式:

for var in list
do
	commands
done

如果你愿意,也可以这么写for var in list ; do

list 参数中要提供迭代要用到的一系列值,在每次迭代中变量var 会包含列表中的当前值,第一次迭代会使用列表的第一个值,第二次使用第二个,以此类推,直到列表的值都过一遍。

讲着很麻烦,来个例子更直观:

#!/bin/bash
for test in 1 2 3 4 5
do
	echo "this is $test"
done

执行结果:

结果

读取列表中的复杂值

举个例子:

#!/bin/bash
for test in i don't know if this'll work
do
	echo "word:$test"
done

结果

引号导致语句读取出现错误,有两种办法解决:

  • 转义符
  • 双引号定义含有单引号的单词
#!/bin/bash
for test in i "don't" know if "this'll" work
do
	echo "word:$test"
done
#!/bin/bash
for test in i don\'t know if this\'ll work
do
	echo "word:$test"
done

结果

当然同理,如果是取两个单词,也可以使用双引号括起来,值得注意的是引号并不会被识别为字符。

从变量读取列表

#!/bin/bash
list="1 2 3 4 5"
for test in $list
do
	echo "this is $test"
done

从命令读取值

举例:我现在创建一个文件,文件中有1~5五个数,就可以使用cat 命令来查看,然后使用到for命令中

示例

注意语句的语法和文件的路径!这个很重要。

更改字段分隔符

有一个特殊的环境变量IFS 叫做内部字段分隔符,默认情况下bash shell会把以下几个当做字段分隔符:

  • 空格
  • 制表符
  • 换行符

修改IFS 的值:

IFS=$'\n'

这样字段分隔符只会识别换行符了,如果要指定多个字符,用冒号:隔开即可。

用通配符读取目录

for 命令遍历目录中的文件,进行这个操作时,必须在文件名或者路径中使用通配符,它会强制shell使用文件扩展匹配,文件扩展匹配是生成匹配指定通配符的文件名或路径名的过程。例如:

#!/bin/bash
for file in $HOME/test/*
do
	if [ -d "$file" ]	#引号是防止文件名中含有特殊字符影响匹配
	then
		echo "$file is a dir"
	elif [ -f "$file" ]
	then
		echo "$file is a file"
	fi
done

输出结果

当然也可以使用多个目录,用空格隔开即可,有一个缺点就是如果你指定的目录不存在,for 语句也会执行遍历,并不会报错,所以最好在执行遍历的时候检查一下该目录是否存在:

#!/bin/bash
for file in $HOME/test/* $HOME/testing
do
	if [ -d "$file" ]
	then
		echo "$file is a dir"
	elif [ -f "$file" ]
	then
		echo "$file is a file"
	else
		echo "$file does not exist"
	fi
done

输出结果

预知后事如何,请听下期更新~