shell 基本语法
jenkins 上构建项目时,经常需要借助 shell 脚本,最近也经常跟服务器打交道,顺便记录些常用命令,方便查阅
语法-变量
# 定义变量 name='dasu' # 使用变量 echo $name # dasu echo "I am ${name}." # I am dasu.
xxx='dasu'
用 key=value
形式定义变量,=
等号两边不能有空格
$xxx 或 ${xxx}
变量名前加个 $
使用变量,大括号省略也可以
语法-字符串
# 字符串使用 name='dasu' name2="dasu" name3=dasu echo "$name $name2 $name3" # dasu dasu dasu # 字符串长度 echo ${#name} #4 # 注意,shell 里都是命令 'dasu' # dasu: command not found # 获取子字符串 echo ${name:0:2} # da # 寻找字符 echo `expr index $name s` # 3 下标从1开始
'dasu'
"dasu"
dasu
单引号、双引号、甚至不加引号都会被作为字符串使用
单引号里的字符串不做任何处理工作,是什么就原样输出
双引号里如果有表达式、有转义符,有变量,会先进行处理,最后再输出,所以字符串的拼接,可以放在双引号内
注意,shell 里都是命令,所以只有当在命令参数、或表达式右值时,字符串才会被当做字符串处理,否则都会被认为命令,从而报找不到 xxx 命令错误
${#xxx}
加个 #
号使用,可以用来获取 xxx 变量字符串的长度
${xxx:1:2}
用来截取子字符串
i=`expr index "$xxx" x`
用来查找子字符,expr 表示后面跟着的是表达式,因为 shell 默认每一行都是命令,所以本身不支持表达式
index 用来查找,后面跟着接收两个参数:原字符串,查找的字符
注意,只找字符,不是找子字符串
`xxx`
和 $(xxx)
因为不加引号也可以被认为是字符串处理,所以在某些场景,需要让脚本解释器知道,这是一串命令,而不是字符串,此时就需要通过 `
反引号,或者 $()
来实现
echo ls # ls,被认为是字符串 echo `ls` # 执行 ls 命令,并将结果输出 echo $(ls) # 执行 ls 命令,并将结果输出
`
反引号内部是一个命令,$()
美元符合加小括号形式,括号内也是表示一个命令
注意,`
或 $()
内部的命令执行之后的结果,会再次作为输入,被当做下一行 shell 脚本命令执行,所以需要注意这个结果是否可以作为命令被执行
`whoami` # root: command not found
因为 whoami
命令执行输出 root,root 又被作为命令执行,就报错了
如果有需求是要将命令执行结果,作为日志输出,这种场景就很适用了
语法-表达式
编程语言都可以通过各种运算符来实现一个个表达式,如算术表达式、赋值表达式等
但由于在 shell 内,都被当做命令来处理,所以正常的运算符无法直接使用,需要借助其他命令或语法实现
expr
a=2 + 2 # +: command not found a=2+2 # 2+2被认为字符串了 a=`expr 2 + 2` # a=4
`
反引号内的会被当做一个命令来执行,因为上面例子是将 expr 命令放在 =
号右侧,如果不加反引号,expr 会被当做字符串处理
有些算术运算符需要加转义符,如乘号 *,大于 >,小于 < 等
算术运算符跟两侧的变量基本都需要以空格隔开,这样才能辨别是字符串还是表达式
expr 2 + 2 # 4,加法运算 expr 2+2 # 2+2,整个被当做字符串处理 expr 2 - 2 # 0,减法运算 expr 2 \* 2 # 4,乘法运算,需要加转义 expr 2 / 2 # 1,除法运算 expr 2 % 2 # 0,取余运算 expr 2 \> 1 # 1,比较运算,需要加转义 expr 2 \< 1 # 0,比较运算,需要加转义 expr 2 == 2 # 1
(()) 和 $(())
(())
双括号内,可以执行表达式,多个表达式之间以 ,
逗号隔开,最后一个表达式会被作为 (())
运算的结果,可以通过在前面加个 $
提取结果
echo $((a=2+2,$a+2)) # 6 echo $a # 4 ((b=$a*2)) # 8, * 号不用加转义符
(())
和 expr 有各自优缺点:
(())
支持语句,即形如((a=2+2))
,但 expr 只支持表达式,expr 2 + 2
(())
里的乘号,大于号等不需要加转义符,expr 需要加转义符(())
只支持整数的运算,不支持字符串、小数的计算,expr 支持- 等等其他未遇到的场景
$[]
简单的算术表达式还有一种写法:
a=$[2+2] # a=4 a=$[2*2] # a=4,不需要加转义符
跟 expr 相比,$[]
好处就是一些运算符无需加转义符
$[]
跟 $(())
很像,一样支持语句,一样支持多个表达式,通过 ,
逗号隔开,一样会将最后一个表达式的值返回,但 $[]
前的 $
符合不能省略
注意:关于 $[]
和 $(())
的理解可能不是很正确,基本没用过,只是在看资料时遇到,顺手测了些数据梳理出来的知识点,以后有使用到,发现错误的地方再来修改。
而且,目前碰到的 shell 脚本的需求场景,更多的是参数的获取、变量的使用,因为需要动态生成命令来执行,这种场景比较多,关于表达式运算的场景比较少,所以先不必过多关注。
语法-条件判断 if
if 的语法:
if condition then command ... elif condition; then command ... else fi
如果想让 then 和 if 同行,那么需要用 ;
分号隔开,同理,fi 如果想跟 else 或 then 同行,也需要 ;
分号隔开,否则会有语法错误
if 的本质其实是检测命令的退出状态,虽然我们经常可以看到这种写法:
if [ 2 -eq 2 ] if [[ 2 == 1 ]] if (( 1 == 1 ))
以上三种,不管是中括号,双中括号,双小括号,其本质都是在运行数学计算命令,既然是命令,就都会有命令的退出状态
命令退出状态有两种,0 是正常,非 0 是异常,同时,可以用 $?
来获取上个命令的执行退出状态,所以可以来试试看:
[ 2 -eq 2 ] echo $? # 0,正常 [ 2 == 1 ] echo $? # 1,非正常 [[ abc == abc ]] echo $? # 0,正常 [[ ab == abc ]] echo $? # 1,非正常 (( 1 == 1 && 1 > 0 )) echo $? # 0,正常 (( 1 == 1 && 1 > 1 )) echo $? # 1,非正常
明白了吗?
其实, if 检测的是命令的退出状态,这也就意味着,if 后面跟随着的 condition 只要是命令就是符合语法的,不必像其他编程语言那样,必须是类似 if ()
这种语法结构,这也就是为什么,你可能看到别人写的很奇怪的 if 代码,比如:
if test 1 -eq 1; then echo true; fi # true if whoami; then echo true; fi # root true
这样一来,即使再看到别人写的 if 代码很奇葩,至少你也知道,它的执行原理是啥了吧,至少也能看懂他那代码的意图了吧
好,虽然清楚了 if 检测的本质其实是命令的退出状态,但最好还是使用良好的编程风格,使用阅读性较好的写法
关系运算符 -eq -ne -gt -lt -ge -le
- 等价于 == != > < >= <=
这些运算符只能用于比较数值类型的数据,且只能用于 []
, [[]]
这两种,(())
不能使用这种运算符。
但使用 []
和 [[]]
这种语法形式时,有个很重要的点,就是中括号内部两侧必须有空格,然后运算符两侧也需要有空格,否则可能就不是预期的行为了:
if [ 1 -eq 1 ]; then echo true; else echo false; fi # true if [ 1-eq2 ]; then echo true; else echo false; fi # true,因为 1-eq2 被当做字符串了,运算符左右需要有空格 if [ 1==2 ]; then echo true; else echo false; fi # true,因为 1==2 被当做字符串了,运算符左右需要有空格
[]
和 [[]]
内部既可以用类似 -eq
这种形式,也可以直接使用 ==
这种方式,后者可以用于比较字符串,前者不能
布尔运算符 ! -o -a
- 分别对应:非运算,或运算,与运算
if [ 1 -eq 1 -a 1 -gt 1 ]; then echo true; else echo false; fi # false if [ 1 -eq 1 -o 1 -gt 1 ]; then echo true; else echo false; fi # true
这些运算符只能适用于 []
,且只能跟关系运算符(-eq, -ne ...)使用
[[]]
以及 (())
都不能使用,且如果类似这样使用 ==
和 -o
,也是不起作用的:
if [ 1 > 2 -a 1 == 1 ]; then echo true; else echo false; fi # true,1 > 2 明明不符合,却返回 true 了,所以 -a 这种运算符不能喝 > 这类运算符合用,但使用 -gt 就是正常的了 if [[ 1 -eq 1 -o 1 -gt 2 ]]; then echo true; else echo false; fi #sh: syntax error in conditional expression #sh: syntax error near `-o' #异常,[[]] 不支持使用布尔运算符
逻辑运算符 && ||
- 逻辑的 AND 和逻辑的 OR
if [[ 1 == 1 && 1 > 2 ]]; then echo true; else echo false; fi # false
这种运算符只能适用于 [[]]
,此时不管是使用 ==
这类运算符,还是 -eq
这类,都是允许的
[]
和 (())
都不适用
当需要有嵌套的判断时,可以拆开,比如:
if [[ 1 == 1 ]] && [[ 1 > 3 || 1 > 0 ]]; then echo true; else echo false; fi # true # 相当于 if ((1==1) && ((1>3)||(1>0)))
字符运算符 = != -z -n $
- = != 用于判断字符串是否相等
- -z 用于判断字符串长度是否为 0,是的话,返回 true
- -n 用于判断字符串长度是否为 0,不是的话,返回 true
- $xxx 用于判断 xxx 字符串是否为空,不为空返回 true
a='abc' if [ $a == absc ]; then echo true; else echo false; fi # true if [ -n $a ]; then echo true; else echo false; fi # true ,因为长度不为0 if [ -z $a ]; then echo true; else echo false; fi # false,因为长度不为0 if [ $a ]; then echo true; else echo false; fi # true
这种运算符适用于 []
和 [[]]
这两种,不适用于 (())
文件测试运算符 -d -r -w -x -s -e
-f 检测文件是否是普通文件(既不是目录,也不是设备文件)
-r 检测文件是否可读
-w 检测文件是否可写
-x 检测文件是否可执行
-s 检测文件是否为空
-e 检测文件是否存在
-d 检测文件是否是目录
a=test.sh if [ -e $a ]; then echo true; else echo false; fi # 检测 test.sh 文件是否存在 if [ -d $a ]; then echo true; else echo false; fi # 检测 test.sh 是否存在且是否是目录
这类运算符适用于 []
和 [[]]
这两种,不适用于 (())
涉及计算的判断条件
大部分场景下,if 的条件判断,使用上述的运算符结合 [[]]
使用就可以了,但有某些场景,比如先进行算术运算之后,再判断结果:
if ((1+1>2)); then echo true; else echo false; fi # false
如果想使用 [[]]
实现,可以是可以,但有些麻烦:
if [[ $[1+1] > 2 ]]; then echo true; else echo false; fi # false
就是需要先让 1+1 当做表达式计算结束,并获取结果,然后再来做判断
(())
有一点需要注意,它只能进行整数运算,不能对小数或字符串进行运算
小结
脚本中使用到 if 条件判断的场景肯定也很多,绝大多数情况下,使用 [[]]
就足够覆盖需求场景了
不管是需要对文件的(目录、存在、大小)判断,还是需要对字符串或命令执行结果的判断,使用 [[]]
都可以实现了
其实,[[]]
可以说是 []
的强化版,后者能办到的,前者都行,而对于 (())
,更多是整数运算表达式的使用场景,拿来结合 if 使用,纯粹是因为刚好遇见而已,并不是专门给 if 设计的,毕竟 if 只检测命令执行结果,只要是命令,都可以跟它搭
语法-函数和参数
- 函调定义
function add() { // ... } # 省略 function 关键字 add(){ echo $* echo ${12} return 1 }
- 函数调用
add 1 2 #sh 1 2
函数调用时,直接函数名即可,如果需要参数,跟其他编程语言不同,定义时不能指明参数,而是函数内部直接通过 $n 来获取参数,需要第几个,n 就是第几
函数调用时,当需要传参时,直接跟在函数名后面,以空格隔开,函数名不需要带括号
参数 $n
$0
$*
$#
读取参数,参数可以是执行脚本时传递的参数,也可以是执行函数时传递的参数
$1
表示第一个参数,以此类推${10}
当参数个数超过 9 个后,需要用大括号来获取$*
或$@
输出所有参数$0
输出脚本文件名$#
输出参数个数
所以,脚本内部开始,可以用 echo $0 $*
来输出外部使用该脚本时,传递的参数
语法-脚本文件的 source 和执行
当前 shell 脚本内,可以导入其他脚本文件,也可以直接执行其他脚本文件
source
当某个脚本被其他脚本导入时,其实相当于从其他文件拷贝脚本代码过来当前脚本环境内执行,导入有两种命令:
. filename # 注意点号 . 和文件名中间有空格 #或者 source filename
被导入的脚本文件不需要是可执行类型的,毕竟执行环境还是当前脚本启动的 shell 进程,只是执行的代码无需再写一遍,直接从其他地方拷贝过来一条条执行而已
执行
在当前脚本内,也可以直接执行其他脚本文件,同样有两种类型,如:
sh ./test.sh echo $? # 脚本执行的退出状态 #或者 ./test.sh
两种的区别就在于:
- 前者不需要被执行的脚本是可执行类型的,因为已经手动指定 sh 来作为脚本解释器了,脚本内部开头的
#!
声明也会失效掉 - 后者的话,纯粹就是执行一个可执行文件的方式,那就需要这个脚本文件是可执行类型的,同时脚本的解释器由脚本文件内部开头的
#!
声明
我们通常都会将不同工作职责写在不同脚本文件中,然后某个脚本文件内,来控制其他脚本文件的执行流程,那么,这时候,就需要知道每个流程的脚本是否执行正常,这时候,就可以借助脚本的 exit 命令和 $?
来实现
每个脚本,如果正常执行结束,那么脚本内部最后应该通过 exit 0
来退出,表示当前脚本正常执行,如果执行过程出异常了,那么应该执行 exit 1
只要是非 0 即可,来表示当前脚本执行异常
那么,调用执行这个脚本的,就可以通过 $?
来获取脚本执行结果,如:
sh ./test.sh if [[ $? -ne 0 ]]; then echo '异常' exit 1 fi
这样就可以来控制脚本执行流程
语法-其他
注释
#xxxx
单个 #
用来注释后面内容
#!/bin/sh
脚本文件的顶行,告诉系统,应该去哪里用哪个解释器执行该脚本;
但如果该脚本不是直接执行,而是作为参数传递给某个解释器,如:
/bin/sh xxx.sh
,那,文件顶头的 #!
声明就会被忽视,毕竟已经明确指定解释器了
for 循环
for loop in 1 3 4 5 6 do done
$?
用来获取上个命令的执行之后的退出状态,或者获取上个函数执行的返回值,0 表示正常,非0 表示不正常
所以,脚本如期结束时,脚本内最后应该 exit 0 来退出命令(每个脚本的执行其实就是执行命令)
read xxx
从标准输入中读取一行,并赋值给 xxx 变量
printf
输出格式化
输入输出
默认的输入输出都是终端,但可通过 >
<
来进行修改,比如
ls > file
将输出写入到文件中,覆盖写入
ls >> file
将输出写入到文件中,追加写入
xxx.sh < file
本来是从键盘输入到终端,转移到从文件读取内容
<<EOF
xxx.sh<<EOF .... EOF
将两个 EOF 之间的内容作为输入
ls > /dev/null
如果希望执行某个命令,但又不希望在屏幕上显示,那么可以将输出重定向到 /dev/null
写入 /dev/null
中的内容会被丢弃
语法-易混淆
有些语法很容易混淆,在这里列一列:
${} 和 $[] 和 $() 和 $(())
name=dasu echo ${name} # dasu,变量的使用 echo $[2+2] # 4,执行算术表达式,可认为作用跟 expr 类似,但两者有各自局限,expr 支持字符串的关系运算等 echo `expr 2 + 2` # 4 echo $((2+2)) # 4,执行整数的算术表达式,可认为作用跟 expr 类似,但两者有各自局限,expr 支持字符串的关系运算等 echo `expr 2 + 2` # 4 echo $(whoami) # root,执行命令,可认为作用跟 `` 反引号类似 echo `whoami` # root
虽然 $
后面可以跟随各种各样符号,来实现不同用途,但其实,都可以归纳为 $
的作用是,提取后面的结果,然后将其作为输入,再次让 shell 解释器处理。
比如说 ${xxx}
,就是将读取变量 xxx 的值,然后输入给解释器:
name=dasu ${name} # dasu: command not found echo ${name} dasu
是吧,就是提取,然后再输入给解释器,其实也就是变量值的替换,将变量替换为实际的值
那么,这么理解的话,()
小括号内的其实就是在执行命令,$()
就是将命令执行结果替换命令;(())
两个小括号内的其实就是在执行表达式,$(())
就是将表达式执行结果替换掉表达式;$[]
同理;
那么,可能你就会有疑问了:
[1+1] # [1+1]: command not found ((1+1)) # 无报错也无输出
知道为什么吗?
因为 (())
是 shell 解释器可以识别的语法,它知道这不是字符串
但 [1+1]
却被解释器当做一整个字符串了,自然就找不到这个命令,shell 解释器能识别的 []
语法应该是,中括号内部两侧需要有空格,此时就不会认为它是字符串了,如:
[ 1+1 ] # 无报错也无输出
当有 $
时,就无需区分字符串的场景了,自然也就可以省略掉空格了,但保留好习惯,都留着空格也是很好的做法
命令和表达式
- 命令是指 shell 支持的命令,比如 ls,pwd,whoami 等等
- 表达式是指通过运算符组合成的各种表达式,如算术表达式,赋值表达式,关系表达式等等
shell 内的每一行代码都是在执行命令,所以直接在 shell 内书写表达式是会执行异常,因为表达式不是命令
一些命令跟传入参数,如 echo xxx,echo 后跟随着会被当做字符串处理,如果想让 xxx 这串被作为命令执行,那需要将 xxx 放置于 `xxx`
反引号或者 $(xxx)
内
如果想让 xxx 被当做表达式处理,则需要借助一些命令,如 expr;
如果表达式是算术表达式,那可通过 ((xxx))
包裹这些表达式,但需要获取表达式结果时,通过 $((xxx))
在前面加个 $
实现
本篇就先介绍一些基础语法吧,当然并不全面,但足够看懂基本的 shell 脚本代码了
下一篇会介绍一些常用命令,如 expect,scp,ssh,以及再拿个 jenkins 上构建项目的实例脚本来讲讲