linux之shell脚本quickStart

发布时间 2023-08-19 14:51:48作者: sureZ_ok

这篇文章主要参考于《跟老男孩学linux运维:Shell编程实战》,方便写shell脚本时参考,只列一些shell脚本中的容易混淆的知识点。

1 变量

变量就是用一个固定的字符串代替更多、更复杂的内容,使用变量的最大好处就是使程序开发更为方便。

1.1 普通变量

变量可分为全局变量和普通变量(局部变量),变量赋值时“=”两边不能加任何空格。

全局变量:可以在创建其shell脚本以及其派生出来的任意子进程shell脚本中使用。全局变量又分为自定义环境变量和bash内置的环境变量。

普通变量:只能在创建它们的shell函数或shell脚本中使用。

设置环境变量方法:

# 方法1:
export 变量名=value

# 方法2:
变量名=value

# 方法3:
declare -x 变量名=value

取消本地变量和环境变量:

unset # 使用unset消除本地变量和环境变量

查看环境变量:

# 方法1:
set # 命令输出所有变量,包括全局变量和局部变量、函数。
env # 命令只显示全局变量

变量赋值时要注意单引号,双引号的细微差别:

名称 说明
单引号 所见即所得,即输出时会将单引号内的所有内容原样输出,单引号看到是什么就是什么。
双引号 如果内容中有命令(要加反引号)、变量、特殊转义符等,会先将其解析,然后输出最终内容
无引号 赋值时,如果变量内容有空格,则会造成赋值不完整,如果内容中有命令(要加反引号),变量、特殊转义符等,会解析输出

建议:shell脚本中,字符串变量一定要加双引号,如果是数字可以不加

输出变量时,可以使用$c 和${c} 两种方法,而$(c) 有别的用途,如下:

把一个命令的结果作为变量的内容赋值:

# 方法1
变量名=`ls`  # 把命令用反引号引起来

# 方法2
变量名=$(ls) # 把命令用$()括起来,推荐这种方法

这一点与Makefile的方式并不相同,不要弄混了。

shell中变量引用与Makefile中的差别:

$x 单字符变量 $xx 或 $xxx_x多字符变量 $ $()
shell 变量取值 变量取值 变量取值 相当于``,解析命令结果
Makefile 变量取值 No 变量取值 变量取值

建议:Makefile中变量一定要写成$()或${}的形式,shell中使用$xx或${}的形式,shell中的$()另有用途。

1.2 shell特殊变量

在shell脚本中,存在一些特殊且重要的变量,例如:$0、$1、$#等。

位置变量 说明
$0 获取当前执行shell脚本的文件名(包括脚本路径)
$n 获取当前执行的shell脚本第n个参数值,n=1..9,当n=0时表示脚本的文件名,如果n>9,则用大括号括起来,如${10},接的参数以空格隔开
$# 获取当前执行的shell脚本后面接的参数总个数
$* 获取当前shell脚本所有传参的参数,不加引号和$@相同,如果给$*加上双引号,例如"$*",则表示将所有参数视为单个字符串,相当于"$1 $2 $3"
$@ 获取当前shell脚本所有传参的参数,不加引号和$*相同,如果给$@加上双引号,例如"$@",则表示将所有参数视为独立字符串,
相当于"$1" "$2" "$3", 这是将多参数传递给其它程序的最好方式,因为它会保留内嵌到参数里的空格。
$* 与 $@相同,但"$*" 与 "$@"两者有区别。
$? $?用于存储上一个命令的执行状态,它的取值范围通常为0-255,其中0表示执行成功,非0表示出现错误

1.3 shell特殊扩展变量

Shell的特殊扩展变量说明如下:

表达式 说明
VAL=$ 如果变量parameter没有定义或者值为空,则返回word字符串并替代变量的值,即VAL=word
用途:可以定义变量的默认值,这样如果变量未定义,则返回默认值
VAL=$ 如果变量parameter没有定义或者值为空,则设置这个变量值为word,并返回其值,
即parameter=word且VAL=word,基本同${parameter:-word},但该变量又额外给parameter变量赋值了
VAL=$ 如果变量parameter没有定义或者值为空,则返回word字符串作为标准错误输出,否则输出变量的值
用途:当变量parameter异常,报错并输出提醒。
VAL=$ 如果变量parameter没有定义或者值为空,则什么都不做,否则返回word字符串并替代变量的值。

在上述表达式内的冒号都是可选的。

2 运算符

2.1 空格

空格的注意事项:

在shell脚本中,空格的使用需要十分注意(不像C、Java等语言对空格不敏感)。

空格有如下注意事项:

  1. 定义变量时,=的两边不可以留空格,因为shell 会认为空格前的为一个命令,但是在()内部不限制,如for ((i= 1;i < 3;i= i+1))是正确的;

  2. (())内外部括号之间无空格,( () )这样报错。但内部括号内不限制,随便如s=$(( $i + 1 ))可以;

  3. 条件测试语句[ ] 的两边都要留空格;

  4. 条件测试的内容,如果是字符串比较的话, 比较符号两边要留空格;

  5. 操作符之间要用空格分开,如 test ! -d $1,其中的!和-d就要用空格分开,空格是命令解析中的重要分隔符;

  6. 命令和其后的参数或对象之间一定要有空格;

  7. 取变量值的符号'$'和后边的变量或括号不能有空格;

2.2 (()) 与 [ ]

(()) 与 [ ]用于条件表达式,区别见第4章,当然(())除了用于条件表达式外,还常用于算术数值运算。

2.3 || 与 &&

有时候,下一条命令依赖前一条命令是否执行成功。如:前一条命令成功执行之后再执行另一条命令,或者前一条命令执行失败后再执行另一条命令等。shell 提供了 && 和 || 来实现命令执行控制的功能,shell 将根据 && 或 || 前面命令的返回值来控制其后面命令的执行,这类似与C语言中的短路逻辑原则。

  1. 前一条命令成功执行之后再执行另一条命令 &&

    语法格式如下:

    command1 && command2 [&& command3 ...]
    

    只有在 && 左边的命令返回真,&& 右边的命令才会被执行。

  2. 前一条命令执行失败后再执行另一条命令 ||

    语法格式如下:

    command1 || command2 [|| command3 ...]
    

    只有在 || 左边的命令返回假,|| 右边的命令才会被执行。

3 常用命令

列出一些shell中常用的命令:

3.1 read

$ read -n2 -p 'please input you choice:\n' var 
-n2表示只接收2个字符,输入回车后,输入值保存到var变量中

3.2 echo

echo -e可以输出换行,可以解析转义字符。

$ echo -e 'aaa\nbbb' 输出:
aaa
bbb

3.3 eval

eval命令对变量进行两次扫描。

使用举例:

$ cat test
Hello World
$ myfile="cat test"
$ echo $myfile
cat test
$ eval $myfile
Hello World
# eval命令将会对该变量进行两次扫瞄。

3.4 双小括号(())

(())常用于整数运算中。shell中常见的算术运算命令如下:

其操作方法如下:

运算命令 意义
((i=i+1)) 此种书写为运算之后赋值,即将i+1的运算结果赋值给变量i
注意不能用echo ((i=i+1))来输出表达式的值,但可以用echo $((i=i+1))输出其值
i = $((i+1)) 可以在(())前加$符,表示将表达式运算后赋值给i
((8 > 7 && 5 == 5)) 可以进行比较操作以及逻辑操作
echo $((2+1)) 需要直接输入表达式的运算结果时,可以在(())前加$符

4 流程控制

4.1 条件表达式

条件表达式一般搭配&&与|| 来使用,进行一些简单的判断,或者搭配if语句来使用,实现一些较复杂的判断逻辑。

4.1.1 条件表达式几种形式

条件测试语法 说明
语法1: test <测试表达式> 利用test命令进行条件测试表达式的方法,test命令和<测试表达式>之间至少有一个空格
语法2:[ <测试表达式> ] 单中括号,和test命令的用法相同,[]的便捷和内容之间至少有一个空格
语法3:[[ <测试表达式> ]] 双中括号,是比test和[]更新的语法,支持通配符,双大括号里的两端也要有空格
语法4:((<测试表达式>)) 双小括号,一般用于if语句里,常用于计算,(())两端不需要有空格

4.1.2 文件测试表达式

常用的文件测试操作符 说明
-e 文件 exit,文件是否存在,区别与"-f"的是,-e不区分是文件夹还是目录
-d 文件 directory,文件存在且为文件夹则为真,即测试表达式成立
-f 文件 file,文件存在且为普通文件则为真,即测试表达式成立
-s 文件 size,文件存在且文件大小不为0则为真
-r 文件 read,文件存在且可读为真
-w 文件 write,文件存在且可写为真
-x 文件 executable, 文件存在且可执行则为真
f1 -nt f2,nt为newerthan newer than, 文件f1比文件f2新则为真,根据文件的修改时间来计算
f1 -ot f2,ot为olderthan older than, 文件f1比文件f2旧则为真,根据文件的修改时间来计算

4.1.3 字符串测试表达式

注意:

  1. 对于字符串的测试,一定要将字符串加双引号后再进行对比,这样避免字符串中有空格的情况;

  2. 比较符号(例如=和!=)的两端一定要有空格。

常用字符串测试操作符 说明
-n “字符串” 若字符串的长度不为0,则为真,即测试表达式成立,n可以理解为nozero
-z “字符串” 若字符串的长度为0,则为真,z可以理解为zero
“串1” = “串2” 若字符串1等于字符串2,则为真,可使用==代替=
“串1” != “串2” 若字符串1不等于字符串2,则为真

4.1.4 整数测试表达式

在[]以及test中使用的比较符号 在(())和[[]]中使用的比较符号 说明
-eq ==或= 相等,全拼为equal
-ne != 不相等,全拼为not equal
-gt > 大于,全拼为greater than
-ge >= 大于等于,全拼为greaterequal
-lt < 小于,全拼为less than
-le <= 小于等于,全拼为less equal

4.1.5 逻辑操作表达式

在[]中使用的逻辑操作符 在test、[[]]和(())中使用的逻辑操作符 说明
-a && and,与,两端都为真,则结果为真
-o ll or,或,两端有一个为真,则结果为真
not,非,两端相反,则结果为真

以下写法适用于所有条件表达式,是工作中比较常用的替代if语句的方法,以[]为例。

[ 条件1 ] && {
	命令1
	命令2
	命令3
}

# 等价于:
if [ 条件1 ]; then
	命令1
	命令2
	命令3
fi

# 同理:
[ 条件1 ] || {
	命令1
	命令2
	命令3
}

# 等价于:
if [ ! 条件1 ]; then
	命令1
	命令2
	命令3
fi

总结:

测试表达式 [] test [[]] (())
边界是否需要空格 需要 需要 需要(指内部[]) 不需要
文件测试表达式 -e、-d、-f 等 -e、-d、-f 等 -e、-d、-f 等 -e、-d、-f 等
字符串比较 =、==、!= =、==、!= =、==、!= =、==、!=
整数比较 -eq、-gt、-lt、-ge、-le -eq、-gt、-lt、-ge、-le -eq、-gt、-lt、-ge、-le 或
=、>、<、>=、<=
=、>、<、>=、<=
逻辑操作表达式 !、-a、-o !、-a、-o !、&&、|| !、&&、||
是否支持通配符 不支持 不支持 支持 不支持

4.2 if 语句

在所有编程语言里,if条件语句几乎是最简单、用途最广的语句格式。

一般的方式为:

# 第一种写法
if <条件表达式>
  then
  指令集1
fi

# 第二种写法,相当于换行
if <条件表达式>; then
  指令
fi

# 第三种写法:if else 结构
if <条件表达式>; then
  指令集1
else
  指令集2
fi

# 第四种写法:多分支结构 if-elif-else 结构,注意if与elif后都带有then,else后面没有then
if <条件表达式1>; then
  指令集1
elif <条件表达式2>; then
  指令集2
else
  指令集3
fi

4.3 case语句

case条件语句相当于多分支的if/elif/else条件语句,但是比条件语句看起来更工整,case语句比较适合变量值较少且为固定数字或字符串集合的情况。

如下:当变量的值等于1时,执行指令1,等于值2时执行指令2,依次类推,如果都不符合,则执行*)分支,此外,注意不同行内容的缩进距离。

case "变量" in
	值1)
		指令1...
		;;           # 双引号相当于c语言switch-case语句中的break
	值2)
		指令2...
		;;
	*)                # 相当于default分支,可以不用双引号;;
		指令3...
esac

4.4 循环结构

循环中常用的是while与for语句,区别是:while循环常用在守护进程,以及那些希望能持续执行不退出的应用。for循环语句主要用于执行次数有限的循环。

# 第一种写法:while循环语句
while <条件表达式>
do
  指令集1
done

# 第二种写法:until循环语句(使用的很少)
# 条件不成立进入循环,条件表达式成立时终止循环
until <条件表达式>
do
  指令集1
done

# 第三种写法:for循环语法结构
# 第一种for循环语句为变量取值型
for 变量名 in 变量取值列表
do
  指令集1
done

# 第二种for循环语句为C语言型for循环结构
for((exp1; exp2; exp3))
do
  指令集1
done

注意:sh并不支持第二种循环语句(C语言型for循环结构),见参考5

5 函数

5.1 shell函数的几种写法

# 第一种写法,function + 函数名 + 括号(推荐)
function 函数名() {
	指令...
	return n
}

# 以下两种方法都是简写,function和() 可以省略一个
# 第二种写法,函数名后无括号
function 函数名 {
	指令...
	return n
}

# 第三种写法,无function
函数名() {
	指令...
	return n
}

return与exit的用法:

return用来退出函数,exit用来退出脚本文件

5.2 shell函数的执行

shell函数执行的方式:

# 1. 函数不带参数时,直接调用函数名即可,也即function + 函数名 + 括号,只保留函数名即可
# 2. shell执行系统中各种程序的执行顺序为:系统别名->函数->系统命令->可执行文件
# 3. 函数需要先定义然后再执行函数,否则会报错
函数名

# 2. 带参数的函数执行方法
# 例如:函数名 $1 $2 $3
函数名 参数1 参数2

6 shell脚本的调试

有时shell脚本不正确,需要调试,可使用如下方法:

方法1:在脚本中加打印

在shell脚本中加echo

方法2:bash参数调试法

bash [-nvx] scripts.sh

其中:

-n :不会执行脚本,仅查询脚本语法是否有问题,并给出错误提示。

-v : 在执行脚本时,先将脚本内容输出到屏幕上,然后执行脚本,如果有错误,也会给出错误提示

-x :将执行的脚本内容及输出显示到屏幕上,这是对调试很有用

bash -x 的缺点: 当加载系统函数库等脚本时,有太多的输出,导致很难查看我们关注的内容,可以在脚本开始叫上set -x ,可以弥补bash -x的缺点,加了set -x后,就不需要用bash -x了。

方法3:线上语法检查工具

https://www.shellcheck.net/

注意:

shell脚本出错常与环境变量设置有关,这时候,可以使用 printenv 打印环境变量。或使用 export -p 命令显示全部拥有导出属性的变量。

7 shell脚本其它

7.1 shell脚本的执行方式

当shell脚本运行时,会先找茬系统环境变量ENV(加载顺序为:/etc/profile -> ~/.bash_profile -> ~/.bashrc->/etc/bashrc),在加载了环境变量文件后,Shell脚本开始执行Shell脚本的内容。

执行脚本有如下几种方式:

方式 说明
bash script-name 或 sh script-name 当脚本本身没有可执行权限时的使用方法,或脚本首行没有指定解释器(推荐)
./script-name 在当前路径下执行脚本,脚本需要有可执行权限
source script-name 或 . script-name 在当前父shell脚本中运行,(其它模式都会启动新进程执行子脚本)
source或者“ . ”相当于include功能,可以将脚本本身的变量值或函数传递到当前父shell脚本中使用。
sh < script-name 或 cat script-name | sh 用法不常见。

其中,source方法的特点是:

  1. 子shell脚本会直接继承父亲的shell脚本变量,函数(就像儿子随父亲姓,基因也继承父亲的),反之则不可以
  2. 如果希望父shell获得子shell脚本定义的变量,函数,需要使用source或. 在父shell脚本中先加载子shell脚本。

7.2 shell脚本开发规范

编程开发规范很重要,遵守规范,养成良好的编程习惯,可以大大提高开发效率,降低脚本维护成本。

# 规则1: shell脚本的第一行,通常用于指定脚本解释器,叫做解释伴随行( Shebang)
#!/bin/bash 或 #!/bin/sh

# 规则2:shell脚本中尽量不要用中文注释,应用英文注释,防止乱码

# 规则3:字符串赋值给变量应加双引号,并且等号前后不能有空格(shell脚本要注意空格的使用,见第二节,空格)

参考:

  1. shell脚本 空格
  2. shell 脚本里的 特殊字符 $(( ))、$( )、``与${ }的区别
  3. shell 中 &&和||的方法
  4. 【总结】超全shell条件测试命令及语法
  5. https://askubuntu.com/questions/400936/loop-variable-error-in-for-loop