Shell脚本实用技巧小结
不够全面,但主要针对于日常工作常用的细节&技巧
前言
Shell脚本
,故名思义,是“外壳程序”,由于用户无法和内核直接对话,给Kernel套了一层外壳语言Shell,通过这个中间层来和系统交互,主要功能是提升各项日常事务的自动化程度,并不像C/C++那般集中于复杂的特定任务计算。
Shell有很多种,$sh$、$csh$、$ksh$、$bash$、$zsh$ 是比较常见的,可以通过echo $SHELL
查看当前使用的Shell是什么,也可以通过chsh
命令来管理终端使用的SHELL类别:
|
|
在Shell脚本的开头,需要指定该脚本使用哪个shell来执行(可以和终端使用的SHELL不同):
|
|
或者开头加上:
|
|
当然,bash是最常用的,建议shell脚本还是用bash,即使在win平台的git-bash,也可以运行bash。
所以,本文提及的Shell语法技巧,是跨平台的,适用于 Windows/Linux/MacOS。
基本语句结构
if
基本用法
if语句应该是使用最多的分支结构了,基本结构是if...then...fi
,多种情况可插入else
和elif
。
注意条件判断的括号可以用中括号[ ]
,也可以用 (( ))
,或者[[ ]]
。
[ ]
和[[ ]]
的区别是,前者使用-a
,-o
表示与、或的逻辑关系,或者用&&
和||
。
(( ))
和[[ ]]
的区别是,前者才可以判断简单数学运算的判断,后者更适合处理字符串比较。
关于空格:
- if和括号之间要留空格
- 括号和内部表达式之间要有空格
- ⚠️shell脚本里的赋值语句,等号=前后不要留空格
|
|
🚀如果有多个分支结构,需要else if的逻辑,那么使用elif ... then
即可。
数值判断
有2种情况:
-
写在
[ ]
或[[ ]]
内部比较数字表达式的大小要注意,只支持
>
,<
,==
,``!=的判断,不支持
>=和
<=的判断。需要使用
-ge和
-le来替换。当然,大于、小于、等于、不等于也可以用
-gt、
-lt、
-eq、
-ne`来替换。📍Ps:gt表示greater than ,ge 表示 greater or equal,lt表示little than,le表示little or equal。
1 2 3 4 5 6
#!/bin/bash a=5 b=6 if [ $a -le $b ] ; then echo "a is little or equal than b" fi
-
写在
(( ))
内部(( ))
内部可以非常完美地支持四则运算,所以关于数字表达式的判断,建议和支持使用这种形式,非常自然地支持>
,<
,>=
,<=
四种符号的判断。
文件属性判断
一般是对文件或者文件夹做判断,常用的类型如下几类:
选项 | 含义 |
---|---|
-e | 判断文件或目录是否存在 |
-d | 判断是不是目录,并是否存在 |
-f | 判断是否是普通文件,并存在 |
-r | 判断文档是否有读权限 |
-w | 判断是否有写权限 |
-x | 判断是否可执行 |
使用时的具体格式举例为:if [ -e filename ] ; then
字符串判断
选项 | 含义 |
---|---|
-z | 判断该字符串是否为空(为空返回真) |
-n | 判断该字符串是否为非空(非空返回真) |
== | 字符串1和 字符串2是否相等 |
!= | 字符串1和 字符串2是否不同 |
=~ | 字符串str和正则表达式reg是否匹配 |
逻辑复合判断
逻辑判断,通常指“与”,“或”,“非”。
-
与:即and,用于
[ ]
内部之间,使用-a
表示;用于[[ ]]
内部之间,使用&&
表示; -
或:即or,用于
[ ]
内部之间,使用-o
表示;用于[[ ]]
内部之间,使用||
表示; -
非:即not,使用
!
表示。
-a 和 -o 作为测试命令的参数用在测试命令的内部,而 && 和 || 则用来运算测试的返回值,! 为两者通用。
for
for循环分为数字型循环和字符型循环。基本结构为for ... do ...done
。
通常,do是独占一行,也可以与for语句同行,但要使用分号
;
隔开。
数字型for
注意注释中另外2种写法建议。
|
|
字符型for
字符型的循环一般用来列表的遍历,或者文件的遍历。
文件的遍历:
|
|
🚥 如果需要递归遍历子目录,需要命令选项-R,即 ls -R
。
字符串的遍历:
|
|
while
shell脚本的while循环和其他语言的逻辑判断一致,当条件不符合时跳出循环。基本语句结构是while... do...done
。
同样地,可以让 do
语句独立成行,也可以while 判别式; do
以分号隔开,实现while和do同一行。
|
|
case
case语句对应其他语言的switch-case结构,只是在写法上有所区别。通常用于针对用户的不同输入,作出不同的分支流程。
举例一个编译脚本需要选择分支,代码如下:
|
|
continue/break
这2者的用法都是结合for/while循环使用的,意义和其他语言类似,此处不赘述。
遍历文件
有时会有需要对文件夹内每个文件都进行相同的处理,这时遍历文件操作是无法避免的了。
|
|
脚本运行计时
系统time指令
主要使用了time
指令,time后接linux指令可以测量指令运行时间。
|
|
具体time指令的用法,可以参考:菜鸟教程:time指令
脚本内计时
|
|
接收命令行参数
命令行特殊变量
通常Shell脚本内可以自定义变量,比如buildDir="build"
,读引用的时候使用$buildDir
或者${buildDir}
。
但还有一类变量不需要定义,每个shell脚本内都可以直接使用,可以理解为built-in 变量。
变量 | 含义 |
---|---|
$0 |
当前脚本的文件名 |
$n |
传递给脚本或函数的参数。n 是一个数字,表示第几个参数 |
$# |
传递给脚本或函数的参数个数。 |
$* |
传递给脚本或函数的所有参数。 |
$@ |
传递给脚本或函数的所有参数。加双引号" 才和$* 有区别,引号中返回每个参数。 |
$? |
上个命令的退出状态,或函数的返回值。 |
$$ |
当前Shell进程ID。对于 Shell 脚本,就是这些脚本所在的进程ID。 |
✳️对比$*
和$@
的区别(使用时要加引号"才会体现区别,不加引号就一样,都是逐个参数多行输出):
|
|
如果执行./testParam.sh a b 1 2 -x -y
,那么输出如下内容:
|
|
命令行参数分析
运行一个脚本可能会带有参数,比如 ./build.sh -d -arm64
的含义可能为“编译arm64架构的debug包”。
在学会getopts
分析之前,我是对$1,$2...$n
逐个分析(n为参数个数$#
),然后利用case语句赋值的,写出来的代码比较冗长麻烦且不美观。
废话不多说,直接看getopts
的使用吧:
while与getops联合使用,待选的选项里有2类,带参数的以冒号:隔开,不带参数的不需分隔,写在一起。示例代码如下所示 ⬇️
|
|
鉴于上述getopts的定义,可以使用./build.sh -h
来查看帮助信息,也可以使用./build.sh -r -a x86_64 -s 10.15
来执行脚本进行build(示例代码略去build脚本其他内容)。
字符串处理
说起字符串,先说个比较常用的,字符串的长度表示为${#str}
:
|
|
字符串截取
Shell脚本中字符串截取分2种情况:使用数字下标,或使用特定字符标记。
-
数字下标截取
这种方式就是选定一个起始点start-pos,然后按照从左往右的阅读顺序截取一段长度为length的子串。
-
从字符串开头截取
形式为
${originStr:start_pos:length}
,起始点为从左边第一个字符(下标0)开始数,数到下标为start_pos的字符,从左往右截取长度为length的字串,length设置过大则到远串末尾截止。 -
从字符串尾部开始截取
形式为
${originStr:0-start_pos:length}
,“0-”是表示从后往前数,起始点为从右边第一个字符(下标1,倒着数,从1开始)开始数,数到下标为start_pos的字符,同样从左往右截取长度为length的字串,length设置过大则到远串末尾截止。1 2 3 4 5 6 7 8 9
从字符串开头截取 #!/bin/bash originStr="https://kissingfire123.github.io" echo "originStr is $originStr" subStr1=${originStr:3:6} # 从字符串开头截取 echo "subStr1 is $subStr1" subStr2=${originStr:0-6:4} # 从字符串尾部开始截取 echo "subStr2 is $subStr2"
运行后输出如下内容:
1 2 3
originStr is https://kissingfire123.github.io subStr1 is ps://k subStr2 is hub.
-
-
特定子串标记
特定字符标记的意思是“遍历字符串,遇到某个目标字符串chars为止,截取其前/其后的部分”。明显,这种方式不能指定长度。
🤔 这种截取方式的截取内容,都不包括分隔符chars。
-
截取左边
使用
%
来截取左边的字符串,其中星号*
表示通配符,注意是char*
,因为需要左边,所有chars的右边内容我们不关心。格式 含义 ${originStr%chars*}
遍历originStr,遇到第一个子字符串chars为止,截取左边内容 ${originStr%%chars*}
遍历originStr,遇到最后一个子字符串chars为止,截取左边内容 示例代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13
#!/bin/bash originStr="https://kissingfire123.github.io" echo "originStr is $originStr" subStr1=${originStr%.*} # 匹配第一个符号'.' echo "subStr1 is $subStr1" subStr2=${originStr%%.*} # 匹配最后一个符号'.' echo "subStr2 is $subStr2" subStr3=${originStr%ss*} # 匹配第一个子字符串'ss' echo "subStr3 is $subStr3"
输出内容为:
1 2 3 4
originStr is https://kissingfire123.github.io subStr1 is https://kissingfire123.github subStr2 is https://kissingfire123 subStr3 is https://ki
-
截取右边
使用
#
来截取左边的字符串。逻辑和上述左边字串的非常类似,注意是*char
,因为需要右边,所有chars的左边内容我们不关心。类似地有下列表格:格式 含义 ${originStr#*chars}
遍历originStr,遇到第一个子字符串chars为止,截取右边内容 ${originStr##*chars}
遍历originStr,遇到最后一个子字符串chars为止,截取右边内容 示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
#!/bin/bash originStr="https://kissingfire123.github.io" echo "originStr is $originStr" subStr1=${originStr#*i} # 寻找第一个字符串'i',保留其右边 echo "subStr1 is $subStr1" subStr2=${originStr##*i} # 寻找最后一个字符串'i',保留其右边 echo "subStr2 is $subStr2" subStr3=${originStr#*ss} # 寻找第一个字符串'ss',保留其右边 echo "subStr3 is $subStr3"
输出如下内容:
1 2 3 4
originStr is https://kissingfire123.github.io subStr1 is ssingfire123.github.io subStr2 is o subStr3 is ingfire123.github.io
-
字符串替换
如果说,上一小节是描述substr功能,那么本小节就是描述replace功能了。
格式 | 含义 |
---|---|
${originStr/match_str/replace_str} |
1个/,表示将匹配到的第一个match_str,替换成replace_str |
${originStr//match_str/replace_str} |
2个/,表示将匹配到的所有match_str,全替换成replace_str |
示例如下:
|
|
输出内容为:
|
|
⚠️:match_str可以为正则表达式,留待下节讲述。
字符串正则
正则表达式,用于模糊查找匹配 ,本身比较复杂,但掌握后受益良多,详细可参考菜鸟教程:正则表达式。 Shell 字符串的正则表达式主要用于2处,一处为字符串比较判断,另一处为字符串的正则匹配后替换。
Shell 脚本中应用正则表达式的几个常用命令为 grep
、sed
、 cut
、 awk
,需要注意的是,具体命令支持的正则功能范围可能是不同的。
-
正则比较判断
正则比较判断的比较符号是
=~
,使用原始字符串和匹配正则pattern进行匹配,如果成功匹配返回1,失败返回0,常用于判断字符类型(比如时间、电话号、版本类型等)。关于 if 语句针对 =~ 的应用,可参考GNU-Bash参考手册: 条件表达式 。 -
正则字符替换
在 sed 指令中使用频繁,在 vim 指令中也可以发挥用武之地,此处不赘述。
下方代码使用 if 表达式 和 grep 简单示范上述2者的用法:
|
|
上述脚本执行后输出如下内容:
|
|
多行注释
多行注释有以下2种方法可以做到 👇
空命令接收注释行
用 “:<<任意字符与数字
” + "相同字符与数字"
将需要注释的多行内容包裹起来即可。注意,这个字符尽量不要取单引号,双引号,以免和注释内容中的引号提前匹配。
下方代码取字符为"EOF",其实换成其他的,比如"Comment"也是一样的。
|
|
值得注意的是,这里是用个空命令来接收注释内容,被注释内容也确实没有在当前终端执行,但是“空命令接收”这一动作还是背后有开销的。
函数定义式
对于要注释的多行内容,将其封装成一个函数,这样在脚本运行时完全没有开销。
还是上述的例子,可以使用函数的形式,只定义,不调用:
|
|
重要的FAQs
-
echo的常用输出选项?
echo -n
: 取消换行符echo -e
:启用反斜杠转义
-
单引号
' '
和双引号" "
的区别?- 单引号:也叫hard quote,关闭所有引用
- 双引号:也叫soft quote,保留$引用
-
变量赋值和export差别?
-
变量赋值:使用等号
=
赋值,等号前后不能留有空格 -
export变量:如果是在Shell脚本内export,即成为当前Shell脚本运行时的环境变量;如果是在终端命令窗口export,有效范围为当前窗口;在
~/.bash_profile
export,有效范围为当前用户;在/etc/bashrc
(Ubuntu为/etc/bash.bashrc),有效范围为本机所有用户。
-
-
exec和source的差异?
我们执行一个shell脚本时,实际是先产生一个sub-shell的子进程,然后sub-shell在去产生命令行的子进程。
./test.sh
:创建sub-shell执行脚本;source test.sh
:以当前shell执行;exec test.sh
:当前shell执行后退出;
-
如何实现命令替换和变量替换?
- 命令替换:使用圆括号
$( )
或者反引号**` `**,将命令包裹其中 - 变量替换:使用大括号
${ }
,将变量包裹其中
- 命令替换:使用圆括号
-
不同情况下的’退出’含义?
- break:是提前结束循环
- return:是结束function
- exit:是结束Shell脚本,并给出返回值
-
如何检测脚本是否执行成功?
第一,当然是打印信息或者直接写result文件了。
其二,Shell中有个特殊的变量
$?
,就是上一句命令或者上一个刚执行完的脚本的返回值,只需要echo $?
,就能快速查看上一条语句/脚本是否成功了。通常来说,脚本执行成功都是约定俗成返回值0。
-
Shell脚本的数组怎么使用?
比如定义一个数组
A=(a b c d)
,那么如下示例:1 2 3 4 5 6 7 8 9 10
# 引用数组 ${A[@]} ${A[*]} # 访问数组成员 ${A[0]} # 数组长度 ${#A[@]} ${#A[*]} # 数组修改赋值 A[2]=xyzw
参考链接: