shell脚本基础

924 阅读5分钟

shell语言是运行在linux系统上的一种脚本语言,学会shell脚本有很多好处,比如可以自己编写一些自动化的脚本去定时执行,或者部署keepalived时编写一个检查脚本

下面我把平时整理的shell脚本知识一一列出来,这些基础知识也可以在终端上执行,并不是只有在shell脚本中执行,这些也会提升我们在终端上操作的能力

特殊字符

字符说明
~home目录,如cd ~表示切换到当前用户的home目录下
-和cd命令配合使用,指上一个目录,如cd -表示返回上一个目录,注意不是返回上一级
.当前目录
..上一级目录
!!执行上一个命令
!h执行最后一个以h开头的命令
$取变量值,如1表示执行命令时输入的第一个参数,1表示执行命令时输入的第一个参数,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.shbash 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[@]}得到所有索引,遍历所有索引就可以遍历整个关联数组

比较大小

整数

比较整数大小,注意不能像其他语言中那样用 a>a > 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