shell脚本
元字符
$符号详解
0 1 # @ * ? $$ 的各种符号意义详解
在linux shell脚本中经常用到字符,下面是的一些常见用法
名称 含义
$# 传给脚本的参数个数
$0 脚本本身的名字
$1 传递给该shell脚本的第一个参数
$2 传递给该shell脚本的第二个参数
$@ 传给脚本的所有参数的列表
$* 以一个单字符串显示所有向脚本传递的参数,与位置变量不同,参数可超过9个
$$ 脚本运行的当前进程ID号
$? 显示最后命令的退出状态,0表示没有错误,其他表示有错误
实例1
建立脚本peng.sh如下:
#/bin/bash
total=$[ $1 * $2 + $3 ]
echo "$1 * $2 + $3 = $total"
123
运行如下:
./peng.sh 4 5 6
1
结果如下:
实例2
当把字符串输入给shell脚本的时候,注意是以空格作为分隔符,如果字符串本身就有空格的话,那么用双引号或者单引号,比如
#/bin/bash
echo "$1 is pretty nice!"
12
运行如下:
./peng.sh 一口Linux
1
结果如下:
实例3
#!/bin/sh
echo "参数个数:$#"
echo "脚本名字:$0"
echo "参数1:$1"
echo "参数2:$2"
echo "所有参数列表:$@"
echo "pid:$$"
if [ $1 = 100 ]
then
echo "命令退出状态:$?"
exit 0 #参数正确,退出状态为0
else
echo "命令退出状态:$?"
exit 1 #参数错误,退出状态1
fi
实例4
使用for循环进行参数遍历
示例:
#!/bin/bash
number=65 #定义一个退出值
index=1 #定义一个计数器
if [ -z "$1" ];then #对用户输入的参数做判断,如果未输入参数则返回脚本的用法并退出,退出值65
echo "Usage:$0 + 参数"
exit $number
fi
echo "listing args with \$*:" #在屏幕输入,在$*中遍历参数
for arg in $*
do
echo "arg: $index = $arg"
let index+=1
done
echo
index=1 #将计数器重新设置为1
echo "listing args with \"\$@\":" #在"$@"中遍历参数
for arg in "$@"
do
echo "arg: $index = $arg"
let index+=1
done
linux命令执行返回值**$?**说明
在 Linux 下,不管你是启动一个桌面程序也好,还是在控制台下运行命令,所有的程序在结束时,都会返回一个数字值,这个值叫做返回值,或者称为错误号 ( Error Number )。
- 在控制台下,有一个特殊的环境变量 $?,保存着前一个程序的返回值,我们可以试试:
先随便执行个命令,比如像上面的 ls 某些文件,然后通过 echo ?,打印 ? 的值~
我们发现返回值是 0,这是什么意思呢?
只要返回值是 0,就代表程序执行成功了~
也就是说,如果 $? 变量的值不是 0 的话,就表示上一个命令在执行的过程中出错了。
- 我们可以试着 ls 一个不存在的文件:
>/dev/null 2>&1
1、可以将/dev/null看作"黑洞". 它非常等价于一个只写文件. 所有写入它的内容都会永远丢失. 而尝试从它那儿读取内容则什么也读不到. 然而, /dev/null对命令行和脚本都非常的有用.
用处:
禁止标准输出. 1 cat $filename >/dev/null # 文件内容丢失,而不会输出到标准输出.
禁止标准错误. 2>/dev/null 这样错误信息[标准错误]就被丢到太平洋去了.
2、1>/dev/null 2>&1的含义
> 代表重定向到哪里,例如:echo "123" > /home/123.txt
1 表示stdout标准输出,系统默认值是1,所以">/dev/null"等同于"1>/dev/null"
2 表示stderr标准错误
& 表示等同于的意思,2>&1,表示2的输出重定向等同于1
那么本文标题的语句:
1>/dev/null 首先表示标准输出重定向到空设备文件,也就是不输出任何信息到终端,说白了就是不显示任何信息。
2>&1 接着,标准错误输出重定向等同于 标准输出,因为之前标准输出已经重定向到了空设备文件,所以标准错误输出也重定向到空设备文件。
分解这个组合:“>/dev/null 2>&1” 为五部分。
1:> 代表重定向到哪里,例如:echo "123" > /home/123.txt
2:/dev/null 代表空设备文件
3:2> 表示stderr标准错误
4:& 表示等同于的意思,2>&1,表示2的输出重定向等同于1
5:1 表示stdout标准输出,系统默认值是1,所以">/dev/null"等同于 "1>/dev/null"
1111
kill
进程状态转换
kill命令回顾
kill :发送指定的信号到相应进程。不指定信号将发送SIGTERM(15)终止指定进程。若仍无法终止该程序可用“-KILL” 参数,其发送的信号为SIGKILL(9) ,将强制结束进程,使用ps命令或者jobs 命令可以查看进程号。root用户将影响用户的进程,非root用户只能影响自己的进程。
常用kill命令
2.1 kill命令参数
在命令行输入kill,可以看到参数提示如下:
kill: usage: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ... or kill -l [sigspec]
参数说明:
-l <信号编号>,若不加信号的编号参数,则使用“-l”参数会列出全部的信号名称。常用的kill -15,kill -9这里的9 和 15就是信号;
-a 当处理当前进程时,不限制命令名和进程号的对应关系;
-p 指定kill 命令只打印相关进程的进程号,而不发送任何信号;
-s 指定发送信号;
-u 指定用户;
2.2 信号
使用kill -l 可以列举所有支持的信号:
[admin@test-3 ~]$ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR111) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+338) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+843) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+1348) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-1253) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-758) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-263) SIGRTMAX-1 64) SIGRTMAX
也可以通过kill -l SIGHUP 查指定信号对应的数值:
[admin@test-3 ~]$ kill -l SIGHUP
1
几个常用的信号如下:
HUP 1 终端断线INT 2 中断(同 Ctrl + C)QUIT 3 退出(同 Ctrl + \)TERM 15 终止KILL 9 强制终止CONT 18 继续(与STOP相反, fg/bg命令)STOP 19 暂停(同 Ctrl + Z)
其中,信号如果没有指定的话,默认会发出终止信号(15)。比较常用的就是强制终止信号:9
和终止信号:15
,另外,中断信号:2
其实就是Ctrl + C结束前台进程。
2.3 kill -15与kill -9
当我们使用kill pid时,实际相当于kill -15 pid。也就是说默认信号为15。使用kill -15
时,系统会发送一个SIGTERM的信号给对应的程序。当程序接收到该信号后,具体要如何处理自己可以决定。
这时候,应用程序可以选择:
- 1、立即停止程序
- 2、释放响应资源后停止程序
- 3、忽略该信号,继续执行程序
因为kill -15
信号只是通知对应的进程要进行"安全、干净的退出",程序接到信号之后,退出前一般会进行一些"准备工作",如资源释放、临时文件清理等等,如果准备工作做完了,再进行程序的终止。
但是,如果在"准备工作"进行过程中,遇到阻塞或者其他问题导致无法成功,那么应用程序可以选择忽略该终止信号。
这也就是为什么我们有的时候使用kill命令是没办法"杀死"应用的原因,因为默认的kill信号是SIGTERM(15),而SIGTERM(15)的信号是可以被阻塞和忽略的。
和kill -15
相比,kill -9
就相对强硬得多,系统会发出SIGKILL信号,他要求接收到该信号的程序应该立即结束运行,不能被阻塞或者忽略。
所以,kill -9在执行时,应用程序是没有时间进行"准备工作"的,所以这通常会带来一些副作用,数据丢失或者终端无法恢复到正常状态等。
单引号和双引号的区别
单引号''和双引号"",两者都是解决变量中间有空格的问题。
在bash中“空格”是一种很特殊的字符,比如在bash中这样定义str=this is String,这样就会报错,为了避免出错就得使用单引号''和双引号""。
单引号'',双引号""的区别是:单引号''剥夺了所有字符的特殊含义,单引号''内就变成了单纯的字符。**双引号""则对于双引号""内的参数替换($)和命令替换(``)**是个例外。
比如说 n=3
echo '$n'
结果就是n3
改成双引号 echo "$n",结果就是3
反引号和$()
反引号,将shell命令引起来,可以将命令的输出值赋给变量
比如这里,date命令输出具体的时间
[root@localhost ~]$ date
Mon Apr 27 16:07:01 CST 2020
那么我用反引号将命令完整的引起来,赋值给变量today,那我们可以输出这个变量,若无再次赋值,那这个变量值是不会改变的。
[root@localhost ~]$ today=`date`
[root@localhost ~]$ echo $today
Mon Apr 27 16:13:36 CST 2020
[root@localhost ~]$ echo $today
Mon Apr 27 16:13:36 CST 2020
要输出某个命令的结果值,直接$美元符引用其实也可以了,是一样的效果
[root@localhost ~]$ echo $(date)
Mon Apr 27 16:20:45 CST 2020
[root@localhost ~]$ today = $(date)
[root@localhost ~]$ echo $today
Mon Apr 27 16:20:59 CST 2020
[root@localhost ~]$ echo $today
Mon Apr 27 16:20:59 CST 2020
$() 与 反引号 `` 其实效果是一样的
在操作上,这两者都是达到相应的效果,但是建议使用$(),理由如下:
1、``反引号容易与双引号弄混,
2、$()更直观
但$()不是所有类在unix系统都支持这种方式,但反引号是肯定支持的。
if逻辑判断
if 逻辑判断。在shell中if判断的基本语法为:
1)不带else
if 判断语句; then
command
fi
例如:
[root@localhost sbin]# cat if1.sh
#! /bin/bash
read -p "Please input your score: " a
if (($a<60)); then
echo "You didn't pass the exam."
fi
在if1.sh中出现了 (($a<60)) 这样的形式,这是shell脚本中特有的格式,用一个小括号或者不用都会报错,
请记住这个格式或者可以写为[ $a lt 60 ]。执行结果为:
[root@localhost sbin]# sh if1.sh
Please input your score: 90
[root@localhost sbin]# sh if1.sh
Please input your score: 33
You didn't pass the exam.
2)带有else
if 判断语句 ; then
command
else
command
fi
例如:
[root@localhost sbin]# cat if2.sh
#! /bin/bash
read -p "Please input your score: " a
if (($a<60)); then
echo "You didn't pass the exam."
else
echo "Good! You passed the exam."
fi
执行结果:
[root@localhost sbin]# sh if2.sh
Please input your score: 80
Good! You passed the exam.
[root@localhost sbin]# sh if2.sh
Please input your score: 25
You didn't pass the exam.
和上一例唯一区别的地方是,如果输入大于等于60的数字会有所提示。
3)带有elif
if 判断语句一 ; then
command
elif 判断语句二; then
command
else
command
fi
例如:
[root@localhost sbin]# cat if3.sh
#! /bin/bash
read -p "Please input your score: " a
if (($a<60)); then
echo "You didn't pass the exam."
elif (($a>=60)) && (($a<85)); then
echo "Good! You pass the exam."
else
echo "very good! Your socre is very high!"
fi
这里的 && 表示 “并且” 的意思,当然也可以使用 || 表示 “或者” 执行结果为:
[root@localhost sbin]# sh if3.sh
Please input your score: 90
very good! Your socre is very high!
[root@localhost sbin]# sh if3.sh
Please input your score: 60
Good! You pass the exam.
以上只是简单的介绍了if语句的结构。
在判断数值大小除了可以用 (( )) 的形式外,还可以使用 [ ] ,但是就不能使用>, < , = 这样的符号了,要使用 -lt (小于),-gt (大于),-le (小于等于),-ge (大于等于),-eq (等于),-ne (不等于)。
[root@localhost sbin]# a=10; if [ $a -lt 5 ]; then echo ok; fi
[root@localhost sbin]# a=10; if [ $a -gt 5 ]; then echo ok; fi
ok
[root@localhost sbin]# a=10; if [ $a -ge 10 ]; then echo ok; fi
ok
[root@localhost sbin]# a=10; if [ $a -eq 10 ]; then echo ok; fi
ok
[root@localhost sbin]# a=10; if [ $a -ne 10 ]; then echo ok; fi
再看看if中使用 && 和 ||的情况:
[root@localhost sbin]# a=10; if [ $a -lt 1 ] || [ $a -gt 5 ]; then echo ok; fi
ok
[root@localhost sbin]# a=10; if [ $a -gt 1 ] || [ $a -lt 10 ]; then echo ok; fi
ok
shell 脚本中if还经常判断关于档案属性,比如判断是普通文件还是目录,判断文件是否有读写执行权限等。常用的也就几个选项:
-e :判断文件或目录是否存在
-d :判断是不是目录,并是否存在
-f :判断是否是普通文件,并存在
-r :判断文档是否有读权限
-w :判断是否有写权限
-x :判断是否可执行
-z : 字符串的长度为0
使用if判断时,具体格式为:
if [ -e filename ] ; then
例子:
[root@localhost sbin]# if [ -d /home/ ]; then echo ok; fi
ok
[root@localhost sbin]# if [ -f /home/ ]; then echo ok; fi
因为 /home/ 为目录为非文件,所以并不会显示 “ok” .
[root@localhost sbin]# if [ -f /root/test.txt ]; then echo ok; fi
ok
[root@localhost sbin]# if [ -r /root/test.txt ]; then echo ok; fi
ok
[root@localhost sbin]# if [ -w /root/test.txt ]; then echo ok; fi
ok
[root@localhost sbin]# if [ -x /root/test.txt ]; then echo ok; fi
[root@localhost sbin]# if [ -e /root/test1.txt ]; then echo ok; fi
if中的括号使用
Shell 里面的中括号(包括单中括号与双中括号)可用于一些条件的测试:
单中括号
基本要素:
Ø [ ] 两个符号左右都要有空格分隔
Ø 内部操作符与操作变量之间要有空格:如 [ “a” = “b” ]
Ø 字符串比较中,> < 需要写成> < 进行转义
Ø [ ] 中字符串或者${}变量尽量使用"" 双引号扩住,避免值未定义引用而出错的好办法
Ø [ ] 中可以使用 –a –o 进行逻辑运算
Ø [ ] 是bash 内置命令:[ is a shell builtin
算术比较, 比如一个变量是否为0, [ var -eq 0 ]。 文件属性测试,比如一个文件是否存在,[ -e var ], 是否是目录,[ -d $var ]。
双中括号
基本要素:
Ø [[ ]] 两个符号左右都要有空格分隔
Ø 内部操作符与操作变量之间要有空格:如 [[ “a” = “b” ]]
Ø 字符串比较中,可以直接使用 > < 无需转义
Ø [[ ]] 中字符串或者${}变量尽量如未使用"" 双引号扩住的话,会进行模式和元字符匹配
在进行字符串比较时,最好使用双中括号 [[ ]]. 因为单中括号可能会导致一些错误,因此最好避开它们。
检查两个字符串是否相同:
[[ $str1 = $str2 ]]
当 str1等于str1等于str2 时,返回真。也就是说,str1 和 str2 包含的文本是一样的。其中的单等于号也可以写成双等于号,也就是说,上面的字符串比较等效于 [[ str1 == str2 ]]。
注意 = 前后有一个空格,如果忘记加空格, 就变成了赋值语句,而非比较关系了。
操作符 | 意义 |
---|---|
[[ $str1 != $str2 ]] | 如果 str1 与 str2 不相同,则返回真 |
[[ -z $str1 ]] | 如果 str1 是空字符串,则返回真 |
[[ -n $str1 ]] | 如果 str1 是非空字符串,则返回真 |
seq
seq命令可以输出连续的数字,或者输出固定间隔的数字,或者输出指定格式的数字,这样说比较模糊,看示例就很容易理解。seq命令最简单的使用方法如下:
上述命令表示连续输出1到5,效果如上所示
seq命令还可以实现步进输出,比如从1开始,每次步进2,最大到10,效果如下:
for循环
循环数字
#!/bin/bash
for((i=1;i<=10;i++))
do
echo $i;
done
#!/bin/bash
for i in $(seq 1 10)
do
echo $i;
done
#!/bin/bash
for i in {1..10}
do
echo $i;
done
shell 脚本中交互输入自动化
背景
最近写自动安装脚本遇到redis-server安装的时候,需要输入3个回车,对此尝试无果,最后google比较满意的解决办法:
shell 脚本需要交互,比如输入回车,输入YES/NO Y/N之类进行选择
Linux 中shell脚本运行时经常需要进行交互,比如安装软件的过程中对license声明的确认,需要输入yes,回车之类的确认信息。这个在自动化安装的时候就会是个问题。
解决方案
1、利用重定向
例:以下的test.sh是要求我们从stdin中分别输入no,name然后将输入的no,name打印出来
root@localhost test]# cat test.sh
#! /bin/bash
read -p "enter number:" no
read -p "enter name:" name
echo you have entered $no, $name
#以下是作为输入的文件内容:
[root@localhost test]# cat input.data
1
lufubo
#然后我们利用重定向来完成交互的自动化:
[root@localhost test]# ./test.sh < input.data
you have entered 1, lufubo
2、利用管道完成交互的自动化
这个就是利用管道特点,让前个命令的输出作为后个命令的输入完成的
也用上面例子举例:
[root@localhost test]# echo -e "1\nlufbo\n" | ./test.sh
you have entered 1, lufbo
上面中的 "1\nlufbo\n"中的\n是换行符的意思,这个比较简单的。
3、利用expect
expect是专门用来交互自动化的工具,但它有可能不是随系统就安装好的,有时需要自己手工安装该命令
查看是否已经安装:rpm -qa | grep expect
具体的示例,后面再提供
shell 中的变量路径
有的时候,cron定时任务 和 本地直接执行shell脚本,结果不相同,主要是因为环境变量不同。
解决方案,就是需要在shell脚本中,指定PATH路径。
1.如何获取到PATH呢?
[root@iotapp3 BakUpload]# echo $PATH
/opt/OpenJdk/Jdk11/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/opt/MinioClient:/root/bin
2.如何在shell脚本中,指定PATH呢?
在脚本中的头部,将上面的path,加到里面,如下:
#!/bin/bash
PATH=/opt/OpenJdk/Jdk11/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/opt/MinioClient:/root/bin
最佳案例
#!/bin/bash
PATH=/opt/OpenJdk/Jdk11/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/opt/MinioClient:/root/bin
TOKEN_VAL=0108f233-538d-4f7d-9d39-75b3ea681dcf
upload_log_dir=/opt/BakUpload
START_DATE_TIME=$(date)
echo $START_DATE_TIME >> $upload_log_dir/log.txt
# MinIO别名
MINIO_ALIAS=hxhbminio
# 要上传的本地文件路径
LOCAL_FILE_PATH=/mnt/nfsclient
# 日期
dd=`date +%Y%m%d`
# 目标MinIO桶名称 指定文件夹的话,用/隔开即可,如下所示
BUCKET_NAME=hxhb-prod/upload/${dd}
#
ORIGIN_FILE_NAME=$(ls -lt ${LOCAL_FILE_PATH} | grep "^d" | head -1 | awk '{print $NF}')
cd $LOCAL_FILE_PATH
# 上传到MinIO后的文件名(可选,如果不指定则与本地文件名相同)
UPLOAD_FILE_NAME=tdengine-xuyi-${ORIGIN_FILE_NAME}.tar.gz
echo "-------准备压缩文件------" >> $upload_log_dir/log.txt
tar zcf $UPLOAD_FILE_NAME $ORIGIN_FILE_NAME
echo "-------压缩完成------" >> $upload_log_dir/log.txt
echo $UPLOAD_FILE_NAME >> $upload_log_dir/log.txt
# 后缀名
SUFFIX_NAME=$(echo "${UPLOAD_FILE_NAME#*.}")
echo $SUFFIX_NAME >> $upload_log_dir/log.txt
FILE_MD5_VAL=$(md5sum ${LOCAL_FILE_PATH}/${UPLOAD_FILE_NAME} | awk '{print $1}')
echo $FILE_MD5_VAL >> $upload_log_dir/log.txt
minioFileUrl=/upload/${dd}/${FILE_MD5_VAL}.${SUFFIX_NAME}
echo $minioFileUrl >> $upload_log_dir/log.txt
filePath=/2ca7c9fc5a09427a8897a3319735ea9b/协同办公/盱眙项目/数据库备份归档库
echo "-------开始上传------" >> $upload_log_dir/log.txt
# 上传文件至MinIO
mc put "${LOCAL_FILE_PATH}/${UPLOAD_FILE_NAME}" "${MINIO_ALIAS}/${BUCKET_NAME}/${FILE_MD5_VAL}.${SUFFIX_NAME}"
echo "-------上传完成------" >> $upload_log_dir/log.txt
curl -X POST http://218.94.128.34:9077/simplefile/filetransfer/appendFileMeta -H "Content-Type:application/json" -H "Authorization:${TOKEN_VAL}" -d '{"identifier":"'"${FILE_MD5_VAL}"'","minioFileUrl":"'"${minioFileUrl}"'","fileName":"'"${UPLOAD_FILE_NAME}"'","filePath":"'"${filePath}"'","persist":true}'
echo "-------元数据追加完成------" >> $upload_log_dir/log.txt
# 检查上传是否成功
if [ $? -eq 0 ]; then
echo "上传成功" >> $upload_log_dir/log.txt
else
echo "上传失败" >> $upload_log_dir/log.txt
exit 1
fi
echo "-------删除临时文件------" >> $upload_log_dir/log.txt
rm -rf ${LOCAL_FILE_PATH}/${UPLOAD_FILE_NAME}
END_DATE_TIME=$(date)
echo $END_DATE_TIME >> $upload_log_dir/log.txt