shell语言是运行在linux系统上的一种脚本语言,学会shell脚本有很多好处,比如可以自己编写一些自动化的脚本去定时执行,或者部署keepalived时编写一个检查脚本
下面我把平时整理的shell脚本知识一一列出来,这些基础知识也可以在终端上执行,并不是只有在shell脚本中执行,这些也会提升我们在终端上操作的能力
特殊字符
字符 | 说明 | |
---|---|---|
~ | home目录,如cd ~ 表示切换到当前用户的home目录下 | |
- | 和cd命令配合使用,指上一个目录,如cd - 表示返回上一个目录,注意不是返回上一级 | |
. | 当前目录 | |
.. | 上一级目录 | |
!! | 执行上一个命令 | |
!h | 执行最后一个以h开头的命令 | |
$ | 取变量值,如a表示取a这个变量,在终端中演示:let a=1+2; echo $a | |
& | 后台执行,如java程序运行时经常使用的nohup java -jar xxx.jar >> xxx.log & | |
* | 通配符,匹配所有字符 | |
? | 匹配除了换行之外的任意一个字符 | |
; | 一行执行多个命令时的分隔符 | |
| | 管道符,表示上一个命令的输出作为下一个命令的输入 | |
\ | 转义符,在终端上执行命令时经常遇到命令太长需要换行,就可以使用\ 来转义换行符,否则按回车命令就执行了 | |
`` | 两个反引号,就是键盘TAB键上面的那个键,用在字符串中写命令,和JS里反引号一样,如echo "today is `date +%F`" | |
' | 单引号,纯字符串,不解释其中的变量,这和PHP一样 | |
" | 双引号,会解释字符串中的变量,也和PHP一样 | |
$? | 获取上一个程序或命令的返回值,echo $?,通常=0表示成功,非0表示失败,如sh脚本中exit 2或c语言中exit(-1)或return 0等 | |
$(()) | 在双括号里写任意数学计算,不支持小数运算,如echo $((1+2)),直接echo 1+2会当成字符串 | |
** | 两个*表示多少次方,后面跟的数字就是几次方,如echo $((2**3)) | |
$# | 获取传入的参数数量,如echo $#,在执行sh xxx.sh 时传入参数sh xxx.sh a 123 ,就会得到2,表示传入了两个参数 | |
$* | 获取传入的所有参数 | |
$_ | 最后执行的一个命令,如echo $_ | |
$0 | 获取当前脚本文件名 | |
$$ | 获取当前脚本的进程ID | |
- | 和表格上面的- 不一样,如`echo 'abc' | cat -,表示cat命令要读取的内容来自标准输出流,这个在istio命令中经常看到,还有 wget -q -O - xxx.com其中的 -O -`就是指明输出流 |
操作符
操作符 | 说明 |
---|---|
覆盖文件内容,如echo 'abc' > test.txt | |
>> | 追加到文件末尾,自动插入一个换行符,如echo 'abc' >> test.txt |
< | 读取右边的内容到内存,然后给左边的命令使用,如wc -l < test.txt ,对于wc -l 命令来说并不知道内容是从test.txt中来的 |
<< | 读取右边大段字符串,支持换行, 和PHP里用法一样,如<< EOF ... ... ... EOF ,以EOF开头,也以EOF结尾,如果左边命令在执行时不同步骤需要多个参数时,会逐行使用EOF里面的内容作为参数值 |
对于<<来举下例子:
读取<<右边的内容,交给左边的cat命令作为其参数,cat命令又输出到test.txt,最终实现的效果就是将EOF中间的内容覆盖写入test.txt中,常用于配置文件覆盖,不想覆盖可以修改cat >>
进行追加
cat > test.txt << EOF
a
b
c
EOF
语法
shell脚本文件扩展名是.sh
,文件内容以#!/bin/bash
或#!/bin/sh
开头,一般都用#!/bin/bash
开头
运行shell脚本时,可通过sh xxx.sh
或bash xxx.sh
,不需要脚本文件有可执行权限
可以直接执行/xxx/xxx.sh
,如果脚本文件在当前目录下需要./xxx.sh
运行,直接运行的方式需要先给脚本文件添加可执行权限chmod u+x xxx.sh
注释使用#
变量
变量名区分大小写,比如定义变量可以这么写a=10
,注意a=10
中间在其他语言的习惯是要加空格,但是shell脚本中不可以
#!/bin/bash
# 定义变量a和b
a=10
b=20
# 求出变量a和b的和
c=$(($a+$b))
echo $c
# 销毁变量
unset a b c
# 销毁后再访问变量
echo $c
数组
基础数组
索引不能自己定义,只能是整数,就是其他语言中的数组
#!/bin/bash
# 定义数组
arr=(1 2 3 4)
# 访问第0个元素
echo ${arr[0]}
# 修改第0个元素
arr[0]=10
echo ${arr[0]}
# 获取数组所有元素
echo ${arr[@]}
# 获取数组元素个数
echo ${#arr[@]}
# 获取数组索引
echo ${!arr[@]}
# 从第1个元素开始
echo ${arr[@]:1}
# 从第1个元素开始取两个元素
echo ${arr[@]:1:2}
遍历数组
#!/bin/bash
arr=(1 2 3 4)
for i in ${arr[@]}; do
echo $i
done
获取目录下所有目录和文件名
#!/bin/bash
arr=(`ls /root`)
for name in ${arr[@]}; do
echo $name
done
假设某个目录下有很多个.jar文件,现在需要依次java -jar启动,就需要遍历目录下所有文件判断扩展名为.jar
#!/bin/bash
arr=(`ls /root`)
for name in ${arr[@]}; do
if [ "${name##*.}"x = "jar"x ]; then
echo $name
# 可以java -jar $name,不过java命令是在/etc/profile中引入的,在shell中需要在for循环外面添加一行source /etc/profile
fi
done
上面"${name##*.}"x
的意思是取name这个变量,##
是贪婪操作符,从左到右一直匹配到最后一个.
并删除.
和左边所有内容,这样就得到了文件扩展名部分,x
是防止字符串为空的报错
当然这种需求也完全可以通过其他很多命令来得到,比如find命令配合通配符查找文件
关联数组
关联数组支持自定义索引,就像Java中的Map或PHP中的数组
#!/bin/bash
# 定义一个关联数组
declare -A map
# 加入元素,如果key=a存在则覆盖,不存在则添加
map[a]=10
map[b]=20
map[c]=$((${map[a]}+${map[b]}))
echo ${map[c]}
关联数据也可以和上面的基础数组一样通过${!map[@]}
得到所有索引,遍历所有索引就可以遍历整个关联数组
比较大小
整数
比较整数大小,注意不能像其他语言中那样用 b这样去比较,如果if (( $a > $b ))
是可以的
运算符 | 说明 |
---|---|
-eq | 等于 |
-gt | 大于 |
-lt | 小于 |
-ge | 大于或等于 |
-le | 小于或等于 |
-ne | 不等于 |
小数
终端和shell脚本中都不支持小数的运算和大小比较,需要通过bc命令来实现
// 会报错
echo $((1.5*10))
// 使用bc命令
echo '1.5*10' | bc
输出15.0
小数大小比较,我们用test命令进行测试
// 比较1.5和2的大小
test `echo '1.5*10' | bc | cut -d '.' -f1` -gt $((2*10));
输出1,1表示测试失败,即左边不大于右边,输出0表示成功
比较方式是两边各乘10,再进行比较,其中cut -d '.' -f1
表示将bc计算得到的15.0以.
进行切割再取第1部分即15,用15和20进行比较这就是整数比较了
字符串
运算符 | 说明 |
---|---|
==或= | 判断两个字符串是否相等,如if [ 'a' == 'b' ]; then |
!= | 字符串不相等 |
-n | 字符串长度是否大于0 |
-z | 字符串长度是否等于0 |
逻辑运算
运算符 | 说明 |
---|---|
&& | 与 |
|| | 或 |
! | 非 |
if
if [ 逻辑判断 ]; then
# 条件成立
fi
if [ 逻辑判断1 ] && [ 逻辑判断2 ]; then
# 条件成立
fi
if [ 逻辑判断 ]; then
# 条件成立
else
# 不成立
fi
if [ 逻辑判断1 ]; then
# 条件1成立
elif [ 逻辑判断2 ]; then
# 条件2成立
else
# 都不成立
fi
# 数学运算并判断,使用(())里面可以写数学运算,比如$a+30>$b
a=10
b=20
if (( $a > $b )); then
echo 'a>b'
else
echo 'a<=b
fi
判断条件也可以用两个中括号包裹[[ ]]
#!/bin/bash
# 判断传入的第1个参数是否以字母t开头
if [[ $1 == t* ]]; then
echo 'start with t'
fi
执行./xxx.sh today
for
#!/bin/bash
for name in `ls /root`; do
echo $name
done
for (( i = 0; i < 10; i++ )); do
echo $i
done
# 死循环
for (( ;; )); do
echo 'a'
done
while
#!/bin/bash
i=0
# 当i小于10时循环
while [ $i -lt 10 ]; do
echo $i
# i++
i=$(($i+1))
done
until
与while相反,当条件不成立时开始循环,条件成立时停止循环
#!/bin/bash
# 循环直至i大于10时停止
until [ $i -gt 10 ]; do
echo $i
# i++
i=$(($i+1))
done
case
就是其他语言中的switch语句,不过这语法比较奇怪,下面以一个智能聊天机器人为例
#!/bin/bash
# 判断传入的第1个参数
case $1 in
# 如果是hello就输出hi
hello)
echo hi
# 两个;;表示一个case结束
;;
# 如果是hi就输出hello
hi)
echo hello
;;
# *表示其他任意情况,就是上面的条件都没有满足
*)
echo don\'t understand
;;
# esac结束switch
esac
循环控制
语句 | 说明 |
---|---|
sleep n | 睡眠指定秒数,如sleep 1 |
continue | 跳过此次循环 |
break | 跳出当前循环 |
break n | 跳出n层循环 |
函数
和其他语言的函数差不多,注意:函数调用必须在函数定义下面,这和其他语言不一样
func1 () {
echo 'i am func1'
}
函数调用,直接写函数名即可
func1
函数参数
#!/bin/bash
# 定义函数
func1 () {
echo i am func1
echo "$1+$2=$(($1+$2))"
}
# 调用函数,并传入两个参数
func1 2 3
目录和文件判断
使用test命令在终端也可以进行测试,如test -d xxx; echo $?
# 判断是否为目录,或目录是否存在
test -d 目录的路径
# 输出上面test的返回结果
echo $?
# 判断目录或文件是否存在
test -e xxx
# 判断是否为文件
test -f xxx
# 判断文件是否存在且可读
test -r xxx
# 判断文件是否存在且不为空
test -s xxx
# 判断文件是否存在且可写
test -w xxx
# 判断文件是否存在且可执行
test -x xxx
# 判断文件是否存在且所有者是当前用户
test -O xxx
# 判断文件是否存在且所属组为当前用户所在组
test -G xxx
# 判断文件1是否比文件2新
test 文件1 -nt 文件2
# 判断文件1是否比文件2旧
test 文件1 -ot 文件2
# 判断两个文件是否相等
test 文件1 -ef 文件2
shell脚本语法检查
在linux上通过vi编辑shell脚本,执行时如果报错,系统给的提示并不明显,不便于查找原因,推荐安装shellcheck来进行语法检查
apt install shellcheck
# 检查语法
shellcheck xxx.sh
引用其他shell文件
可以引用其他写好的shell文件,通常是引入函数库文件
#!/bin/bash
# 点号+空格+文件名或目录名
. /xxx/functions
示例 - 判断java进程是否存活
通过ps -ef | grep xxx | grep -v grep
查找java进程,再wc -l
计算行数,如果等于0就表示没有该java进程
#!/bin/bash
if [ $(ps -ef | grep xxx | grep -v grep | wc -l) -eq 0 ]; then
echo "`date +\"%F %H:%M:%S\"` starting java..." >> /usr/local/jar/daemon.log;
source /etc/profile;
nohup java JVM参数 -jar /usr/local/jar/xxx.jar --spring.profiles.active=prod >> /usr/local/jar/xxx.log &
exit 0;
fi
示例 - 批量关闭java程序
通过java自带的jps命令可以列出当前正在运行的java进程,如下:
1230 jar
4560 jar
7890 jps
看到这种格式就可以想到使用awk来获取第1列和第2列,判断第2列是jar就返回第1列,交给kill命令
#!/bin/bash
jps | awk '{if($2=="jar") print $1}' | xargs kill