目录
前言
本篇介绍shell脚本,内容皆总结摘抄自《鸟哥的linux私房菜:基础学习篇》,仅作笔记。
一、shell脚本简介
shell script即shell 脚本,其中shell是在命令行模式下与系统沟通的一个工具接口,script是脚本的意思。shell脚本就是利用shell的功能写一个程序,这个程序是使用纯文本文件,将一些shell的语法和命令写在里面,搭配正则表达式、管道命令和数据流重定向功能,以达到我们想要的处理目的。
shell脚本可以一次执行多个命令,或者利用一些运算与逻辑判断来完成某种功能。虽然shell脚本是一个文本文件,但要编写它的内容还是需要具备一些bash命令的相关知识,部分bash命令前面的文章介绍过此处不再赘述。shell脚本的编写需要注意以下几点:
- 命令是从上而下、从左而右地分析与执行。
- 命令、选项与参数间的多个空格都会被忽略。
- 空白行也会被忽略,且tab键所产生的空白同样视为空格,因此也会被忽略。
- 如果一行的内容太多,可以使用\Enter来扩展至下一行。
- #可作为注释,任何加在#后面的数据都被视为注释文字而被忽略。
一个简单的仅输出hello world的shell脚本内容如下:
#!/bin/bash
# Description:
# it is just a shell script for testing.
# History:
# 2021/07/14 charles test
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
export PATH
echo -e "hello world \a \n"
exit 0
以上程序可以分为以下几种结构:
- 第一行#!/bin/bash声明这个脚本使用的shell名称。因为我们使用的是bash,因此必须要用#!/bin/bash来声明这个文件使用bash的语法。这样以#!开头的行被称为shebang行。当这个程序被执行时,它就能够加载bash的相关环境配置文件(一般是非登陆shell的./bashsrc)并且执行bash来使我们下面的命令能够执行。
- 程序内容说明。除了第一行的#!是用来声明shell的类型,其他的#都是用作注释。一般来说,编写shell脚本时都需要说明该脚本的:①内容与功能;②版本信息;③作者与联系方式;④文件创建日期;⑤历史记录等等。
- 主要环境变量的声明。程序执行前最好先将一些重要的环境变量设置好,这样程序在执行时就可以直接执行一些外部命令,而不用写绝对路径。
- 主要程序部分。这个就是为了达到某种目的或效果编写的程序代码。
- 程序返回值。之前的文章中介绍过#?变量保存了上一个命令的执行结果,且当执行结果为0是表示程序正常执行,否则表示执行报错。在shell脚本中我们也可以使用exit命令来让程序中断,并且返回一个值给系统。除了正常执行返回0外,我们还可以自定义错误信息来返回不同的值。
shell脚本写好之后下面要做的就是执行了,执行方式包括直接命令执行和以bash程序执行。直接命令执行需要要执行的文件具有可读与可执行的权限(rx),包括以下几种:
- 绝对路径,例如使用/var/tmp/test来执行命令;
- 相对路径,假设工作目录在/var/tmp目录,则可以使用./test命令来执行;
- 变量$PATH功能,将test放在$PATH变量指定的目录内。
以上容易有疑问的是相对路径,当工作目录在shell脚本目录时仍需要使用相对路径”./“而不是直接执行,这是为了避免执行到其他的同名命令,与前面的文章介绍过命令查找顺序有关。以bash程序执行可以通过"sh test"或“bash test”执行,使用这种方式执行shell脚本权限只需要读权限(r)即可。
除了以上提到的几种,脚本的执行还可以使用source命令来执行,并且与上面的执行方式是有差异的。不论是使用绝对路径/相对路径还是#PATH,亦或是使用bash来执行脚本,该脚本都会使用一个新的bash环境来执行脚本内的命令。也就是说使用这种方式是,其实脚本是在子进程的bash中执行的。在前面的文章中提到过,在子进程完成后,在子进程内的各项变量或操作将会结束而不会传回父进程中。例如我们编写一个新的shell.sh脚本,内容如下:
#!/bin/sh
# Program:
# a son thread for testing.
# History:
# 2021/07/14 charles test
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
export PATH
read -p "Please input your name:" name
read -p "Please input your gender:" gender
echo -e "My name is ${name} and my gender is ${gender}"
当我们使用sh方式来执行此脚本后,在父进程中试图获取变量name和gender失败。
使用source命令则是直接在父进程中执行,因此执行完上述脚本后仍可以拿到变量name和gender。
二、判断式
1.test命令
test命令可以说是专门用作条件判断的,使用ls /varrr;echo $?命令来判断某个文件是否存在的场景如果使用test命令就变得极其简单。test命令的主要几种功能以及选项参数的说明如下表:
选项参数 | 说明 |
---|---|
1.判断某文件名是否存在并且是否为某种类型 | |
-e | 文件名是否存在 |
-f | 文件名是否存在且类型为文件file |
-d | 文件名是否存在且类型为目录directory |
-b | 文件名是否存在且类型为block device设备 |
-c | 文件名是否存在且类型为character device设备 |
-S | 文件名是否存在且类型为socket文件 |
-p | 文件名是否存在且类型为FIFO(pipe)文件 |
-L | 文件名是否存在且类型为链接文件 |
2.判断某文件名是否存在并且具有某种权限 | |
-r | 文件名是否存在且具有可读权限 |
-w | 文件是否存在且且具有可写权限 |
-x | 文件是否存在且且具有可执行权限 |
-u | 文件是否存在且且具有SUID权限 |
-g | 文件是否存在且且具有SGID权限 |
-k | 文件是否存在且且具有Sticky bit权限 |
-s | 文件是否存在且为非空文件 |
3.两个文件之间的比较 | |
-nt | newer than 判断file1是否比file2新,test file1 -nt file2 |
-ot | older than 判断file1是否比file2旧 |
-ef | 判断file1与file2是否为同一个文件,可用在判断hard link的判定上,主要意义在于判定两个文件是否均指向同一个innode |
4.关于两个整数之间的判定 | |
-eq | equal 两个数的值是否相等,test n1 -eq n2 |
-nq | not equal 两个数的值是否不等 |
-gt | greater than n1是否大于n2 |
-lt | less than n1是否小于n2 |
-ge | greater than or equal n1是否大于等于n2 |
-le | less than or equal n1是否小于等于n2 |
5.判断字符串 | |
test -z string | 判断字符串string是否为0?若string为空字符串,则为true |
test -n string | 判断字符串是否为非0?若string为空字符串,则为false |
test str1 == str2 | 判断str1是否等于str2,若相等则返回true |
test str1 != str2 | 判断str1是否不等于str2,若相等则返回false |
6.多重条件判定 | |
-a | and 两条件同时成立,例如test -r file -a -x file,则file同时具有r和x权限时,才返回true |
-o | or 两条件任何一个成立,例如test -r file -o -x file,则file具有r或x权限时,就会返回true |
! | 反相状态,例如test ! -x file,当file不具有x权限时,返回true |
使用test命令以及前面介绍过的特殊符号来尝试写个shell脚本。首先让用户输入一个文件名,然后判断这个文件是否存在。若不存在则返回“Filename does not exist”信息,并中断程序;若存在,则判断它是个文件还是目录,并输出“Filename is regular file”或“Filename is directory”,然后输出执行者对这个文件或目录所拥有的权限并输出权限数据。
#!/bin/bash
#program:
# a tested shell script for 'test' command's testing.
# History:
# 2021/07/15 charles test
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
export PATH
# input a file name
read -p "Please input a file name:" fileName
# judge the fileName valid
test -z ${fileName} && echo "Please input a non empty file name." && exit 0
# judge the fileName whether existed
test ! -e ${fileName} && echo "Filename does not exist." && exit 0
# judge the fileName is a file or a directory
test -f ${fileName} && fileType="file"
test -d ${fileName} && fileType="directory"
test -r ${fileName} && authority="r"
test -w ${fileName} && authority="${authority}w"
test -x ${fileName} && authority="${authority}x"
echo "The fileName is a ${fileType} and the permissions for you are: ${authority}."
执行这段脚本,分别传入不存在的文件、存在的目录以及存在的文件作为参数,输出结果如下:
可以看到是符合我们的预期的。笔者开始写的脚本输入合法的目录仍然会直接退出,最后发现还是对前面的文章提到过的“&&”和“||”的使用不太熟悉,因此建议尝试自己多练习几次,实在写不出来再看结果以加深印象。
2、判断符号[]
中括号[]也可以用来做条件判断,且用法与test命令类似。例如判断某个变量是否为空可以使用命令“[ -z ""${name} ]”:
中括号在多个地方被使用,例如通配符和正则表达式等。因此在bash脚本中使用中括号作为判断式时,需要注意以下几点:
- 中括号内的每个组件之间都需要用空格来分隔,组件与中括号[]也需要;
- 在中括号内的变量,最好都以双引号括起来;
- 中括号内的常数,最好都以单引号或双引号括起来。
3、shell脚本的默认变量
许多命令后面可以跟选项或参数,shell脚本同样也可以。脚本针对参数已经设置了一些默认的变量名称,对应如下:
/path/scriptnames opt1 opt2 opt3 opt4
$0 $1 $2 $3 $4
执行的脚本文件名即$0变量,后面的参数按照顺序取递增的变量名称,例如“$1”,“$2”等。除了这些数字变量外,还有一些特殊的变量可以在脚本内使用来调用这些参数:
- $#:代表脚本后接的参数的个数,不包括表示脚本文件名称的$0变量,例如上面的例子中$#的值为4;
- $@:表示脚本后接的所有参数,每个变量是独立的,例如上面的例子$@可以表示为opt1 opt2 opt3 opt4;
- $*:表示脚本后接的所有参数,可指定分隔符,默认为空格,例如上面的例子$*可以表示为opt1 opt2 opt3 opt4。
利用这几个变量尝试写个脚本,执行脚本后屏幕打印信息的逻辑如下:
- 程序的文件名;
- 参数数量;
- 如果参数数量小于1个,则输出参数数量太少;
- 全部参数的值。
脚本内容如下:
#!/bash/bin
# Program:
# default parameters's test,like $0,$1,$#,$@,and so on
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/usr/application/java/jdk1.8.0_301/bin:/usr/application/nodejs/node-v14.17.3-linux-x64/bin:/root/bin
export PATH
echo "The scriptname is => $0"
echo "The parameter's number is => $#"
[ "$#" -eq 0 ] && echo "The parameter's number is too little..." && exit 0
echo "The parameter is
执行脚本分别传入0个参数个多个参数,屏幕打印信息如下:
shift命令可以将默认参数的值向前偏移,类似于数据结构队列中的出队pop操作。例如修改上面的脚本,在脚本最后加入以下内容:
shift
echo "The parameter is => $@"
shift 2
echo "The parameter is => $@"
第一行shift就是将变量$1的值设置为变量$2的值,变量$2的值设置为变量$3的值,依次类推。另外,shift命令后还可以接数字,表示偏移量,shift 2表示偏移量为2个。执行脚本传入5个变量屏幕打印信息如下:
下面介绍shell脚本常使用的条件判断式和循环,由于要介绍的几种语法与大多数高级程序语言中的语法类似,因此仅简单介绍语法,不再写脚本示例。
三、条件判断式
1、if..then
if...then是最常见的条件判断式,当符合某个条件判断的时候就执行某项任务。只有一个判断式要判断时,其使用格式如下:
if 条件判断式;then
当条件判断式成立时,执行的命令
fi
在同一个数据的判断中,如果该数据需要进行多种不同的判断时,可以使用以下格式:
if 条件判断式;then
当条件判断式成立时可执行的命令
else
当条件判断式不成立时可执行的命令
fi
或者更复杂的情况需要使用以下格式:
if 条件判断式一;then
当条件判断式一成立时可执行的命令
elif 条件判断式二;then
当条件判断式二成立时可执行的命令
...
else
当前面的条件判断式都不成立时所执行的命令
fi
2、case ... esac
case...esac判断与java中的switch...case语句功能类似,根据变量值的不同执行不同的命令。使用格式如下:
case 变量 in
”变量内容1“)
变量的值为变量内容1时要执行的命令
;;
”变量内容2“)
变量的值为变量内容2时要执行的命令
;;
...
esac
3、函数function功能
函数在脚本中可以封装一些命令从而达到一处定义多处调用的目的,极大地简化了程序代码。function的语法如下:
function 函数名(){
程序段
}
前面提到过shell脚本的执行方式是由上向下、由左而右,因此为了保证脚本中可以调用我们定义的函数,应该把函数定义在脚本的最前面。function也拥有内置变量,且它的内置变量与shell脚本很类似,函数名称使用变量$0表示,后续接的其他变量按照顺序分别使用变量$1、$2...表示。
四、循环
循环,即不断地执行某个程序段落,知道用户设置的条件完成为止。根据某种条件判断式来判断是否完成的循环叫做不定循环;相对应的,固定要跑多少次的循环叫做固定循环。
1、不定循环
不定循环包括while do done和until do done两种语法。
while do done
while do done的使用语法如下:
while 条件
do <====循环开始
循环执行的程序段落
done <====循环结束
以上这种循环表示当条件成立时就进行循环,直到条件不成立结束循环。
until do done
until do done的使用语法如下:
until 条件
do <====循环开始
循环执行的程序段落
done <====循环结束
这种结束循环的方式与while相反,表示当条件成立时就终止循环,否则就一直循环。
2、固定循环
固定循环使用语法for...do...done,这种语法是在执行时就已经知道了要进行几次循环,语法如下:
for 变量名称 in 变量值1 变量值2 变量值3 变量值4 变量值5...
do <====循环开始
循环执行的程序段落
done <====循环结束
上面的多个变量值也可以使用包含了多个值的变量来代替。for循环还可以使用以下语法:
for (( 初始值; 限制值; 赋值运算 ))
do
循环执行的程序段落
done
初始值表示某个变量在循环当中的起始值,类似i=1;限制值表示当变量的值在这个限制值的范围内的时候继续执行循环,例如i<100,否则结束循环;赋值运算表示每做一次循环变量的递增或递减,例如i=i+1。
总结
- shell脚本用在系统管理上是很好的一种工具,但用在处理大量数值运算上就不太行了。因为shell脚本的速度较慢,且使用的CPU资源较多,会造成主机资源的分配不良。
- shell脚本的执行,至少需要有r的权限;若需要执行命令,则需要用于r和x的权限。
- 脚本如果使用source命令来执行,则代表在父程序的bash内执行。
- 可以使用“sh -x 脚本名称”来进行程序的debug。