十分钟带你学会 Shell 脚本

前言
本篇文章没有太多的理论知识,主要分为基础语法案例、常用工具型命令(重点:帮助我们完成复杂需求)、工作中常见的需求(实战案例有源码 , 工作中可以直接套用)模板函数的声明与实现,建议花十分钟阅读一遍收藏即可,当工作中需要编写 Shell 脚本直接套用案例中的脚本模板,足可满足后端开发的大部分需求 。
作为一名后端程序员,如果不掌握基础的 Shell 脚本 , 那么运维编写的一些简单的脚本根本无法看懂,也不便于与运维进行沟通交流 。掌握 Shell,可以帮助我们提高日常工作效率,比如快速构建部署项目、管理集群、监控服务器、定时清理日志文件或管理服务器等等 。
概述
Shell 是由 C 语言编写而成,外号俗称壳 。开发者如果想操作 Linux 系统内核,必须通过 Shell 脚本进行交互,解释和执行用户命令,不可以绕过 Shell 直接操作 Linux 内核 。Shell 是一门强大的编程语言,容易上手功能强大 。
Shell 解析器
Linux 中有几种常见的解析器,后面的模板都是使用 Bash(最常用的解析器)解析器进行编写,查看当前系统支持哪些解析器:
cat /etc/shells
查看当前系统使用的 Shell 解析器:
echo $SHELL
基础语法与实操案例Shell 变量
对于后台开发者 , 系统环境变量一定不会陌生,这里不做过多赘述 。Shell 变量分为两种:系统变量、自定义变量 。
系统变量
常见的系统变量如下:
变量名
解释
$PWD
脚本执行的当前所在目录
$UID
当前操作的系统用户 ID
$$
当前操作用户的 PID
$#
当前脚本的参数个数
$*
当前脚本的所有参数
$0
当前执行程序的名称
$n
当前程序的第 N 个参数
$HOME
当前程序的 home 目录
$USER
查询当前程序使用的操作用户
自定义变量
1. 变量命令规则
变量名必须是以字母或下划线字符“_”开头,后面字母、数字或下划线字符 。切记不用使用特殊符号 , 给自己带来不必要的麻烦 。
2. 查看当前 Shell 所有的环境变量
3. 编写自定义变量
# 变量名=值如:A=1等号两边不要有空格,如果值中间存在空格,请使用单引或者双引号:A='张 三'# 撤销变量unset A# 定义静态变量 , 静态变量不可以二次赋值,静态变量不可以 unset 撤销readonly B=2
4. 变量的作用域
普通的变量作用域为当前的执行程序,程序外部不可使用当前定义的变量 。通过可以把变量升级为全局环境变量,这样当前系统所有程序都可以使用这个环境变量 。
创建测试脚本:
touch test.sh
赋值执行权限:
chmod u+x test.sh
编写脚本:
vim test.sh
定义全局脚本(脚本内容如下):
export user_name="张三"
#!/bin/bashecho $user_name
5. 由于定义了全局变量,所以执行脚本可以正常输出 $ 变量的值,反之脚本中定义的局部变量,其它脚本中不可以正常输出结果 。
./test.sh
运算符
运算符的种类大致可以分为(直接上代码示例)4 种 。
算数运算符
#!/bin/basha=10b=20# 加法val=`expr $a + $b`echo "a + b : $val"# 减法val=`expr $a - $b`echo "a - b : $val"# 乘法val=`expr $a * $b`echo "a * b : $val"# 除法val=`expr $b / $a`echo "b / a : $val"# 取余val=`expr $b % $a`echo "b % a : $val"# 等于if [ $a == $b ]thenecho "a 等于 b"fiif [ $a != $b ]thenecho "a 不等于 b"fi
关系运算符
#!/bin/basha=10b=20# 等于if [ $a -eq $b ]thenecho "$a -eq $b : a 等于 b"elseecho "$a -eq $b: a 不等于 b"fi# 不等于if [ $a -ne $b ]thenecho "$a -ne $b: a 不等于 b"elseecho "$a -ne $b : a 等于 b"fi# 大于if [ $a -gt $b ]thenecho "$a -gt $b: a 大于 b"elseecho "$a -gt $b: a 不大于 b"fi# 小于if [ $a -lt $b ]thenecho "$a -lt $b: a 小于 b"elseecho "$a -lt $b: a 不小于 b"fi# 大于等于if [ $a -ge $b ]thenecho "$a -ge $b: a 大于或等于 b"elseecho "$a -ge $b: a 小于 b"fi# 小于等于if [ $a -le $b ]thenecho "$a -le $b: a 小于或等于 b"elseecho "$a -le $b: a 大于 b"fi
布尔运算符
#!/bin/basha=10b=20# !非运算,跟 java 一样if [ $a != $b ]thenecho "$a != $b : a 不等于 b"elseecho "$a == $b: a 等于 b"fi# 与运算,跟 java 里面的 && 一样if [ $a -lt 100 -a $b -gt 15 ]thenecho "$a 小于 100 且 $b 大于 15 : 返回 true"elseecho "$a 小于 100 且 $b 大于 15 : 返回 false"fi# 或运算,与 java 里面的 || 同理if [ $a -lt 100 -o $b -gt 100 ]thenecho "$a 小于 100 或 $b 大于 100 : 返回 true"elseecho "$a 小于 100 或 $b 大于 100 : 返回 false"fiif [ $a -lt 5 -o $b -gt 100 ]thenecho "$a 小于 5 或 $b 大于 100 : 返回 true"elseecho "$a 小于 5 或 $b 大于 100 : 返回 false"fi
字符串运算符
#!/bin/basha="abc"b="efg"# 判断字符串是否相等if [ $a = $b ]thenecho "$a = $b : a 等于 b"elseecho "$a = $b: a 不等于 b"fi# 判断字符串不相等if [ $a != $b ]thenecho "$a != $b : a 不等于 b"elseecho "$a != $b: a 等于 b"fi# -n 判断字符串长度是否不为 0if [ -n "$a" ]thenecho "-n $a : 字符串长度不为 0"elseecho "-n $a : 字符串长度为 0"fi# 与 -n 相反if [ -z $a ]thenecho "-z $a : 字符串长度为 0"elseecho "-z $a : 字符串长度不为 0"fi# $ 表示检查字符串是否为空if [ $a ]thenecho "$a : 字符串不为空"elseecho "$a : 字符串为空"fi
流程控制
if else 不再做介绍,上述运算符案例中有大量使用,对于后端开发及其简单,流程控制在程序用使用非常频繁 。
case 语法直接套用
最后的 *) 表示默认模式,相当于 Java 中的 ,;; 表示命令序列结束 , 相当于 Java 中的 break 。
!/bin/bashcase $1 in"1")echo "张三";;"2")echo "李四";;*)echo "王二";;esac
for 循环
案例:从 1 加到 100 。
#!/bin/bashs=0for((i=0;i<=100;i++))dos=$[$s+$i]doneecho $s
while 循环
案例:从 1 加到 100 。
#!/bin/bashs=0i=1while [ $i -le 100 ]dos=$[$s+$i]i=$[$i+1]done# 输出值echo $s
函数
Shell 脚本和其它编程语言类似,分为系统函数和自定义函数 。
系统函数
1.基本语法
basename 路径 后缀
功能描述: 命令会删掉所有的前缀包括最后一个(‘/’)字符,然后将字符串显示出来 。
不加后缀:
加后缀:
如果脚本中需要获取当前路径的后缀名称:
2.基本语法
dirname 文件绝对路径
功能描述:从给定的包含绝对路径的文件名中去除文件名(非目录的部分),然后返回剩下的路径(目录的部分) 。
自定义函数
1. 基本语法:
[ function ] funname[()]{Action;[return int;]}
2. 经验技巧
3. 案例实操
函数无返回值:计算两个输入参数的和 。
脚本源码:
#!/bin/bashfunction sum(){s=0s=$[ $1 + $2 ]echo "$s"}# read 读取控制台的输入,n1,n2 用于接收输入内容 , -p:指定读取值时的提示符; -t:指定读取值时等待的时间(秒)read -p "Please input the number1: " n1;read -p "Please input the number2: " n2;# 调用方法sum $n1 $n2;
函数有返回值:计算两个输入参数的和(函数返回值,只能通过$?系统变量获得) 。
#!/bin/bashfunction sum(){# read 读取控制台的输入 , n1,n2 用于接收输入内容,-p:指定读取值时的提示符; -t:指定读取值时等待的时间(秒)read -p "Please input the number1: " n1;read -p "Please input the number2: " n2;return $(($n1+$n2))}# 调用方法sumecho "计算两个数字之和为 $? !"
常用的 Shell 工具
下面列举的几个命令非常实用,命令的具体使用方法请阅读:Linux 命令大全,非常重要且命令参数太多,这里不做过多赘述 。
开箱即用的 Shell 脚本
请用 Shell 脚本写出查找当前文件夹(/home)下所有的文本文件内容中包含有字符“shen”的文件名称 。
grep -r "shen" /home | cut -d ":" -f 1
判断用户输入的是否为 IP 地址:
#!/bin/bashfunction check_ip(){IP=$1VALID_CHECK=$(echo $IP|awk -F. '$1< =255&&$2<=255&&$3<=255&&$4/dev/null; thenif [ $VALID_CHECK == "yes" ]; thenecho "$IP available."elseecho "$IP not available!"fielseecho "Format error!"fi}check_ip 192.168.1.1check_ip 256.1.1.1
定时清空文件内容 , 定时记录文件大?。?
#!/bin/bash#每小时执行一次脚本(任务计划) , 当时间为 0 点或 12 点时,将目标目录下的所有文件内#容清空 , 但不删除文件 , #其他时间则只统计各个文件的大?。?桓鑫募?恍?,输出到以时#间和日期命名的文件中,需要考虑目标目录下二级、三级等子目录的文件logfile=/tmp/`date +%H-%F`.logn=`date +%H`if [ $n -eq 00 ] || [ $n -eq 12 ]then#通过 for 循环,以 find 命令作为遍历条件,将目标目录下的所有文件进行遍历并做相应操作for i in `find /data/log/ -type f`dotrue > $idoneelsefor i in `find /data/log/ -type f`dodu -sh $i >> $logfiledonefi
检测网卡流量,并按规定格式记录在日志中:
#!/bin/bash########################################################检测网卡流量 , 并按规定格式记录在日志中#规定一分钟记录一次#日志格式如下所示:#2019-08-12 20:40#ens33 input: 1234bps#ens33 output: 1235bps######################################################3while :do#设置语言为英文 , 保障输出结果是英文,否则会出现 bugLANG=enlogfile=/tmp/`date +%d`.log#将下面执行的命令结果输出重定向到 logfile 日志中exec >> $logfiledate +"%F %H:%M"#sar 命令统计的流量单位为 kb/s,日志格式为 bps,因此要*1000*8sar -n DEV 1 59|grep Average|grep ens33|awk '{print $2,"t","input:","t",$5*1000*8,"bps","n",$2,"t","output:","t",$6*1000*8,"bps"}'echo "####################"#因为执行 sar 命令需要 59 秒,因此不需要 sleepdone
计算文档每行出现的数字个数 , 并计算整个文档的数字总数:
#!/bin/bash##########################################################计算文档每行出现的数字个数,并计算整个文档的数字总数#########################################################使用 awk 只输出文档行数(截取第一段)n=`wc -l a.txt|awk '{print $1}'`sum=0#文档中每一行可能存在空格,因此不能直接用文档内容进行遍历for i in `seq 1 $n`do#输出的行用变量表示时,需要用双引号line=`sed -n "$i"p a.txt`#wc -L 选项,统计最长行的长度n_n=`echo $line|sed s'/[^0-9]//'g|wc -L`echo $n_nsum=$[$sum+$n_n]doneecho "sum:$sum"
杀死所有脚本:
#!/bin/bash#################################################################有一些脚本加入到了 cron 之中 , 存在脚本尚未运行完毕又有新任务需要执行的情况 , #导致系统负载升高,因此可通过编写脚本,筛选出影响负载的进程一次性全部杀死 。################################################################ps aux|grep 指定进程名|grep -v grep|awk '{print $2}'|xargs kill -9
从 FTP 服务器下载文件:
#!/bin/bashif [ $# -ne 1 ]; thenecho "Usage: $0 filename"fidir=$(dirname $1)file=$(basename $1)ftp -n -v << EOF# -n 自动登录open 192.168.1.10# ftp 服务器user admin passwordbinary# 设置 ftp 传输模式为二进制,避免 MD5 值不同或.tar.gz 压缩包格式错误cd $dirget "$file"EOF
监测 Nginx 访问日志 404 情况:
#场景:#1.访问日志文件的路径:/data/log/access.log#2.脚本死循环 , 每 10 秒检测一次,10 秒的日志条数为 300 条,出现 404 的比例不低于 10%(30 条)则需要重启 php-fpm 服务#3.重启命令为:/etc/init.d/php-fpm restart#!/bin/bash############################################################监测 Nginx 访问日志 404 情况,并做相应动作###########################################################log=/data/log/access.logN=30 #设定阈值while :do#查看访问日志的最新 300 条 , 并统计 404 的次数err=`tail -n 300 $log |grep -c '404" '`if [ $err -ge $N ]then/etc/init.d/php-fpm restart 2> /dev/null#设定 60s 延迟防止脚本 bug 导致无限重启 php-fpm 服务sleep 60fisleep 10done
自动屏蔽访问网站频繁的 IP
方法 1:根据访问日志(Nginx 为例) 。
#!/bin/bashDATE=$(date +%d/%b/%Y:%H:%M)ABNORMAL_IP=$(tail -n5000 access.log |grep $DATE |awk '{a[$1]++}END{for(i in a)if(a[i]>100)print i}')#先 tail 防止文件过大,读取慢,数字可调整每分钟最大的访问量 。awk 不能直接过滤日志 , 因为包含特殊字符 。for IP in $ABNORMAL_IP; doif [ $(iptables -vnL |grep -c "$IP") -eq 0 ]; theniptables -I INPUT -s $IP -j DROPfidone
方法 2:通过 TCP 建立的连接 。
#!/bin/bashABNORMAL_IP=$(netstat -an |awk '$4~/:80$/ && $6~/ESTABLISHED/{gsub(/:[0-9]+/,"",$5);{a[$5]++}}END{for(i in a)if(a[i]>100)print i}')#gsub 是将第五列(客户端 IP)的冒号和端口去掉for IP in $ABNORMAL_IP; doif [ $(iptables -vnL |grep -c "$IP") -eq 0 ]; theniptables -I INPUT -s $IP -j DROPfidone
实现 SSH 免交互执行命令:
登录脚本:# cat login.exp#!/usr/bin/expectset ip [lindex $argv 0]set user [lindex $argv 1]set passwd [lindex $argv 2]set cmd [lindex $argv 3]if { $argc != 4 } {puts "Usage: expect login.exp ip user passwd"exit 1}set timeout 30spawn ssh $user@$ipexpect {"(yes/no)" {send "yesr"; exp_continue}"password:" {send "$passwdr"}}expect "$user@*"{send "$cmdr"}expect "$user@*"{send "exitr"}expect eof
执行命令脚本:写个循环可以批量操作多台服务器 。
#!/bin/bashHOST_INFO=user_info.txtfor ip in $(awk '{print $1}' $HOST_INFO)douser=$(awk -v I="$ip" 'I==$1{print $2}' $HOST_INFO)pass=$(awk -v I="$ip" 'I==$1{print $3}' $HOST_INFO)expect login.exp $ip $user $pass $1done
Linux 主机 SSH 连接信息:
# cat user_info.txt192.168.1.120 root 123456
创建 10 个用户,并分别设置密码模板函数的声明与实现,密码要求 10 位且包含大小写字母以及数字,最后需要把每个用户的密码存在指定文件中:
#!/bin/bash###############################################################创建 10 个用户,并分别设置密码,密码要求 10 位且包含大小写字母以及数字#最后需要把每个用户的密码存在指定文件中#前提条件:安装 mkpasswd 命令###############################################################生成 10 个用户的序列(00-09)for u in `seq -w 0 09`do#创建用户useradd user_$u#生成密码p=`mkpasswd -s 0 -l 10`#从标准输入中读取密码进行修改(不安全)echo $p|passwd --stdin user_$u#常规修改密码echo -e "$pn$p"|passwd user_$u#将创建的用户及对应的密码记录到日志文件中echo "user_$u $p" >> /tmp/userpassworddone
扫描主机端口状态:
#!/bin/bashHOST=$1PORT="22 25 80 8080"for PORT in $PORT; doif echo &>/dev/null > /dev/tcp/$HOST/$PORT; thenecho "$PORT open"elseecho "$PORT close"fidone用 Shell 打印示例语句中字母数小于 6 的单词#示例语句:#Bash also interprets a number of multi-character options.#!/bin/bash###############################################################Shell 打印示例语句中字母数小于 6 的单词##############################################################for s in Bash also interprets a number of multi-character options.don=`echo $s|wc -c`if [ $n -lt 6 ]thenecho $sfidone
【十分钟带你学会 Shell 脚本】本文到此结束,希望对大家有所帮助 。