目录

shell编程

概述

shell是什么

Shell是一个命令行解释器,它为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用Shell来启动、挂起、停止甚至是编写一些程序。

Shell 是一个用C语言编写的程序,它是用户使用Linux的桥梁。Shell既是一种命令语言,又是一种程序设计语言。

每个人在成功登陆Linux后,系统会出现不同的提示符号,比如#,$等,之后用户就可以输入需要的命令,让linux系统执行一直到注销系统为止,在从登录到注销期间,输入的每个命令都会经过解释及执行,负责该用户和Linux系统对话的机制就是shell

Shell 脚本(shell script),是一种为shell编写的脚本程序。其实作为命令语言互动式的解释和执行用户的输入命令只是shell功能的一个方面。shell还能用来进行程序设计,它提供了定义变量和参数的手段以及丰富的程序控制结构。类似于Dos系统中的批处理文件,称为shell script。

为什么要学习shell

  • Linux运维工程师在进行服务器集群管理时,需要编写Shell程序来进行服务器管理。
  • 对于JavaEE和Python程序员来说,工作可能需要编写一些Shell脚本进行程序或者是服务器的维护,比如编写一个定时备份数据库的脚本。
  • 对于大数据程序员来说,需要编写Shell程序来管理集群。

查看shell

当前使用shell

env | grep sh

当前有哪些shell

ls -l /bin/*sh*

Linux的Shell种类众多,常见的有:

  • Bourne Shell(/usr/bin/sh或/bin/sh)
  • Bourne Again Shell(/bin/bash)
  • C Shell(/usr/bin/csh)
  • K Shell(/usr/bin/ksh)
  • Shell for Root(/sbin/sh)
  • ……

我们关注的是 Bash,也就是 Bourne Again Shell,由于易用和免费,Bash在日常工作中被广泛使用。同时,Bash也是大多数Linux系统默认的Shell。 在一般情况下,人们并不区分 Bourne Shell 和 Bourne Again Shell,所以,像 #!/bin/sh,它同样也可以改为#!/bin/bash#!告诉系统其后路径所指定的程序即是解释此脚本文件的Shell程序。

修改shell

chsh -s /bin/bash

shell脚本的执行方式

脚本格式要求

  • 脚本以#!/bin/bash开头
  • 脚本需要有可执行权限
#!/usr/bin/env bash和#!/usr/bin/bash的比较

通过/usr/bin/env 运行程序,用户不需要去寻找程序在系统中的位置(因为在不同的系统,命令或程序存放的位置可能不同),只要程序在你的$PATH中;

通过/usr/bin/env 运行程序另一个好处是,它会根据你的环境寻找并运行默认的版本,提供灵活性。

不好的地方是,有可能在一个多用户的系统中,别人在你的$PATH中放置了一个bash,可能出现错误。

大部分情况下,/usr/bin/env是优先选择的,因为它提供了灵活性,特别是你想在不同的版本下运行这个脚本;而指定具体位置的方式#! /usr/bin/bash,在某些情况下更安全,因为它限制了代码注入的可能。

脚本的常用执行方式

  1. 输入脚本的绝对路径或相对路径

    1. 首先要赋予helloworld.sh 脚本的+x权限
    2. 执行脚本
  2. sh+脚本

    说明:不用赋予脚本+x权限,直接执行即可

注释

单行注释:#

多行注释::<<!开头,结尾

在shell脚本中执行命令

先来说一下主要以下有几种方式:

  • fork: 如果脚本有执行权限的话,path/to/foo.sh。如果没有,sh path/to/foo.sh。
  • exec: exec path/to/foo.sh
  • source: source path/to/foo.sh

fork

fork 是最普通的, 就是直接在脚本里面用 path/to/foo.sh 来调用 foo.sh 这个脚本,比如如果是 foo.sh 在当前目录下,就是 ./foo.sh。运行的时候 terminal 会新开一个子 Shell 执行脚本 foo.sh,子 Shell 执行的时候, 父 Shell 还在。子 Shell 执行完毕后返回父 Shell。 子 Shell 从父 Shell 继承环境变量,但是子 Shell 中的环境变量不会带回父 Shell。

exec

exec fork 不同,不需要新开一个子 Shell 来执行被调用的脚本. 被调用的脚本与父脚本在同一个 Shell 内执行。但是使用 exec 调用一个新脚本以后, 父脚本中 exec 行之后的内容就不会再执行了。这是 exec source 的区别.

source

fork 的区别是不新开一个子 Shell 来执行被调用的脚本,而是在同一个 Shell 中执行. 所以被调用的脚本中声明的变量和环境变量, 都可以在主脚本中进行获取和使用。

shell变量

Shell的变量的介绍

  • Linux Shell中的变量分为,系统变量和用户自定义变量。
  • 系统变量:$HOME$PWD$SHELL$USER等等
  • 显示当前shell中所有变量:set

用户自定义变量

基本语法

  • 定义变量:变量=值
  • 撤销变量:unset 变量
  • 声明静态变量:readonly 变量,注意:不能 unset
  • 可把变量提升为全局环境变量,可供其他 shell 程序使用(参考系统变量)

定义变量的规则

  • 变量名称可以由字母、数字和下划线组成,但是不能以数字开头。
  • 等号两侧不能有空格
  • 变量名称一般习惯为大写

将命令的返回值赋给变量

  • A=`ls -la` 反引号,运行里面的命令,并把结果返回给变量 A
  • A=$(ls -la) 等价于反引号 (推荐)

系统变量

基本语法

  1. 在配置文件(eg /etc/profile)中执行export 变量名=变量值 (功能描述:将 shell 变量输出为环境变量)
  2. source 配置文件 (功能描述:让修改后的配置信息立即生效)
  3. echo $变量名 (功能描述:查询环境变量的值)

位置参数变量

当我们执行一个 shell 脚本时,如果希望获取到命令行的参数信息,就可以使用到位置参数变 量,比如 : ./myshell.sh 100 200 , 这个就是一个执行 shell 的命令行,可以在 myshell 脚本中获取到参数信息

基本语法

  • $n (功能描述:n 为数字,$0 代表命令本身,$1-$9 代表第一到第九个参数,十以上的参数,十 以上的参数需要用大括号包含,如${10}
  • $* (功能描述:这个变量代表命令行中所有的参数,$*把所有的参数看成一个整体)
  • $@(功能描述:这个变量也代表命令行中所有的参数,不过$@把每个参数区分对待)
  • $#(功能描述:这个变量代表命令行中所有参数的个数)

预定义变量

就是 shell 设计者事先已经定义好的变量,可以直接在 shell 脚本中使用

基本语法

  • $$ (功能描述:当前进程的进程号(PID))
  • $! (功能描述:后台运行的最后一个进程的进程号(PID))
  • $? (功能描述:最后一次执行的命令的返回状态。如果这个变量的值为 0,证明上一个命令 正确执行;如果这个变量的值为非 0(具体是哪个数,由命令自己来决定),则证明上一个命令执行不正确了。)

应用实例

在一个 shell 脚本中简单使用一下预定义变量

1
2
3
4
5
6
7
#!/usr/bin/env bash

echo "当前进程号=$$"
# 后台的方式运行tomcat,这个属于演示后台运行,startup.sh本来就运行在后台
exec /Users/david/soft/apache-tomcat-8.5.16/bin/startup.sh &
echo "最后的进程号=$!"
echo "执行结果=$?"

运行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ ./test.sh
当前进程号=81529
最后的进程号=81530
执行结果=0
# david @ Davids-Macbook-Pro in /tmp/com.eh [19:10:57]
$ ./test.sh
当前进程号=81529
最后的进程号=81530
执行结果=0

# david @ Davids-Macbook-Pro in /tmp/com.eh [19:11:13]
$ Tomcat started.
#阻塞中...

运算符

基本语法

  • $((运算式))

  • $[运算式], 推荐写法

  • expr 运算时

    注意 expr 运算符间要有空格,运算符:\*, /, % 乘,除,取余

应用案例

  • 计算(2+3)X4 的值
  • 请求出命令行的两个参数[整数]的和

编写脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#!/usr/bin/env bash

:<<!
echo "当前进程号=$$"
# 后台的方式运行tomcat,这个属于演示后台运行,startup.sh本来就运行在后台
exec /Users/david/soft/apache-tomcat-8.5.16/bin/startup.sh &
echo "最后的进程号=$!"
echo "执行结果=$?"
!


echo "$[(2+3)*4]"

echo "result=$[$1+$2]"

测试脚本:

1
2
3
$ ./test.sh 100 200
20
result=300

条件判断

基本语法

[ condition ](注意condition前后要有空格)

非空返回true,可使用$?验证(0为true,>1为false)

  • [ “sss” ] 返回true

  • [] 返回false

  • [ condition ] && echo OK || echo notok 条件满足,执行后面的语句,否则执行或运算后面的语句

    eg:

    1
    2
    3
    4
    5
    
    $ [ a = b ] && echo OK || echo notOK
    notOK
      
    $ [ a = a ] && echo OK || echo notOK
    OK
    

判断语句

字符串比较

= 字符串比较

两个整数比较

  • -lt 小于
  • -le 小于等于
  • -eq 等于
  • -gt 大于
  • -ge 大于等于
  • -ne 不等于

按照文件权限进行判断

  • -r 有读的权限
  • -w 有写的权限
  • -x 有执行的权限

按照文件类型进行判断

  • -f 文件存在并且是一个常规的文件
  • -e 文件存在
  • -d 文件存在并是一个目录

演示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
$ cat ./test.sh
#!/usr/bin/env bash

# "ok"是否等于"ok"
if [ "ok" = "ok1" ]
then
	echo "等于"
fi

# 23 是否大于等于 22

if [ 23 -gt 22 ]
then
	echo "大于"
fi

# /tmp/com.eh/hello.txt 目录中的文件是否存在
if [ -e /tmp/com.eh/hello.txt ]
then
	echo "存在"
fi

运行结果

1
2
3
$ ./test.sh
大于
存在

流程控制

if

基本语法

1
2
3
if [ 条件判断式 ];then 
	程序 
fi

或者

1
2
3
4
5
6
7
if [ 条件判断式 ] 
then 
	程序 
elif [ 条件判断式 ]
then 
	程序
fi

推荐使用第二种

演示

编写一个 shell 程序,如果输入的参数,大于等于 60,则输出 “及格了”,如果小于 60, 则输出 “不及格”

1
2
3
4
5
6
7
8
9
#!/usr/bin/env bash

if [ $1 -ge 60 ]
then
	echo "及格了"
elif [ $1 -lt 60 ]
then
	echo "不及格"
fi

运行结果:

1
2
3
4
5
6
$ ./test.sh 10
不及格

# david @ Davids-Macbook-Pro in /tmp/com.eh [21:29:55]
$ ./test.sh 70
及格了

case

基本语法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
case $变量名 in 
"值 1") 
如果变量的值等于值 1,则执行程序 1 
;; 
"值 2") 如果变量的值等于值 2,则执行程序 2 
;; 
…省略其他分支…
*) 
如果变量的值都不是以上的值,则执行此程序
;;
esac

演示

当命令行参数是 1 时,输出 “周一”, 是 2 时,就输出"周二", 其它情况输出"other"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/usr/bin/env bash

case $1 in
"1")
echo "周一"
;;
"2")
echo "周二"
;;
*)
echo "other"
;;
esac

运行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ ./test.sh 1
周一

# david @ Davids-Macbook-Pro in /tmp/com.eh [21:34:23]
$ ./test.sh 2
周二

# david @ Davids-Macbook-Pro in /tmp/com.eh [21:34:24]
$ ./test.sh 3
other

for

基本语法

1
2
3
4
for 变量 in 值 12 值 3…
do 
	程序 
done

或者

1
2
3
4
for (( 初始值;循环控制条件;变量变化 )) 
do 
	程序 
done

演示

案例 1 :打印命令行输入的参数【会使用到$* $@

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/usr/bin/env bash

# 使用 $*
for i in "$*"
do
	echo "the num is $i"
done

echo "==================="

# 使用 $@
for j in "$@"
do
	echo "the num is $j"
done

运行结果:

1
2
3
4
5
6
$ ./test.sh 1 2 3
the num is 1 2 3
===================
the num is 1
the num is 2
the num is 3

案例 2:从 1 加到 100 的值输出显示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/usr/bin/env bash

SUM=0

for (( i=1; i<=100; i++ ))
do
	SUM=$[$SUM + $i]
done

echo "sum=$SUM"

运行结果:

1
2
$ ./test.sh
sum=5050

while

基本语法

1
2
3
4
while [ 条件判断式 ] 
do 
	程序 
done

演示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/usr/bin/env bash

SUM=0
i=0

while [ $i -le $1 ]
do
	SUM=$[$SUM + $i]
	i=$[$i+1]
done

echo "sum = $SUM"

运行结果

1
2
$ ./test.sh 100
sum = 5050

read读取控制台输入

基本语法

read (选项) (参数)

选项:

  • -p:指定读取值时的提示符;
  • -t:指定读取值时等待的时间(秒),如果没有在指定的时间内输入,就不再等待了。。

参数:指定读取值的变量名

演示

案例 1:读取控制台输入一个 num 值

案例 2:读取控制台输入一个 num 值,在 10 秒内输入。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/usr/bin/env bash

# 案例 1:读取控制台输入一个 num 值

read -p "请输入一个整数:" NUM1
echo "你输入的数是: $NUM1"

# 案例 2:读取控制台输入一个 num 值,在 10 秒内输入。

read -t 5 -p "请在5秒内输入一个整数:" NUM2
echo "你输入的数是: $NUM2"

运行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ ./test.sh
请输入一个整数:1
你输入的数是: 1
请在5秒内输入一个整数:2
你输入的数是: 2

# david @ Davids-Macbook-Pro in /tmp/com.eh [21:50:44]
$ ./test.sh
请输入一个整数:1
你输入的数是: 1
请在5秒内输入一个整数:你输入的数是:

函数

shell 编程和其它编程语言一样,有系统函数,也可以自定义函数。系统函数中,我们这里就介绍 两个。

系统函数

basename

基本语法:basename [pathname] [suffix]

功能:返回完整路径最后 / 的部分,常用于获取文件名

注意

basename [string] [suffix]

功能描述:basename 命令会删掉所有的前缀包括最后一个(‘/’)字符,然后将字符串显示出来。

选项: suffix 为后缀,如果 suffix 被指定了,basename 会将 pathname 或 string 中的 suffix 去掉。

dirname

基本语法:dirname 文件绝对路径

功能:返回完整路径最后 / 的前面的部分,常用于返回路径部分

演示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# david @ Davids-Macbook-Pro in /tmp/com.eh [21:51:08]
$ basename /home/a/b/c/d.txt
d.txt

# david @ Davids-Macbook-Pro in /tmp/com.eh [22:01:05]
$ basename /home/a/b/c/d.txt .txt
d

# david @ Davids-Macbook-Pro in /tmp/com.eh [22:01:17]
$ dirname /home/a/b/c/d.txt
/home/a/b/c

# david @ Davids-Macbook-Pro in /tmp/com.eh [13:08:46] C:1
$ basename `pwd`
com.eh

# david @ Davids-Macbook-Pro in /tmp/com.eh [13:09:26]
$ dirname `pwd`
/tmp

自定义函数

基本语法

1
2
3
4
5
[ function ] funname[()]
{
	action;
	[return int;]
}

直接写函数名:funname [值]

演示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/usr/bin/env bash

getSum() {
	SUM=$[$n1+$n2]
	echo "sum=$SUM"
}

read -p "n1=" n1
read -p "n2=" n2

# 调用getSum
getSum $n1 $n2

运行结果:

1
2
3
4
$ ./test.sh
n1=1
n2=2
sum=3

shell编程综合案例

需求分析

  • 每天凌晨 2:10 备份 数据库 atguiguDB 到 /data/backup/db
  • 备份开始和备份结束能够给出相应的提示信息
  • 备份后的文件要求以备份时间为文件名,并打包成 .tar.gz 的形式,比如: 2018-03-12_230201.tar.gz
  • 在备份的同时,检查是否有 10 天前备份的数据库文件,如果有就将其删除。

shell脚本如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/bin/sh

# 完成数据库的定时备份
#备份的路径
BACKUP_DIR=/var/lib/mysql

# 当前时间作为文件名
DATETIME=$(date +%Y_%m_%d_%H_%M%S)

# 输出变量调试
echo ${DATETIME}

echo "==========开始备份=========="
echo "==========备份目标文件地址:$BACKUP_DIR/$DATETIME.tar.gz=========="

# 主机
HOST=localhost
# 用户名
DB_USER=root
# 密码
DB_PWD=333
# 备份的数据库名
DATABASE=db01
# 创建备份的路径
# 如果备份的路径文件夹存在就使用,否则就创建
[ ! -d "$BACKUP_DIR/$DATETIME" ] || mkdir -p "$BACKUP_DIR/$DATETIME"

echo "============开始执行备份============"
# 执行mysql备份数据库指令
echo "mysqldump -u${DB_USER} -p${DB_PWD} --host=$HOST $DATABASE | gzip > $BACKUP_DIR/$DATETIME/$DATETIME.sql.gz"
mysqldump -u${DB_USER} -p${DB_PWD} --host=$HOST $DATABASE | gzip > $BACKUP_DIR/$DATETIME/$DATETIME.sql.gz

# 打包备份文件
cd $BACKUP_DIR
tar -zcvf $DATETIME.tar.gz $DATETIME
# 删除临时文件
rm -rf $BACKUP_DIR/$DATETIME

# 删除10天前的备份文件
find $BACKUP_DIR -mtime +10 -name "*.tar.gz" -exec rm -rf {} \;

echo "========备份文件成功==========="

在mysql容器里执行如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
root@1e0e6663ce42:/var/lib/mysql# ./mysql_db_backup.sh
2020_10_07_15_3925
==========开始备份==========
==========备份目标文件地址:/var/lib/mysql/2020_10_07_15_3925.tar.gz==========
============开始执行备份============
mysqldump -uroot -p333 --host=localhost db01 | gzip > /var/lib/mysql/2020_10_07_15_3925/2020_10_07_15_3925.sql.gz
mysqldump: [Warning] Using a password on the command line interface can be insecure.
2020_10_07_15_3925/
2020_10_07_15_3925/2020_10_07_15_3925.sql.gz
========备份文件成功===========

配置任务处理器

1
2
3
4
5
6
7
8
9
$ crontab -e
crontab: installing new crontab

# 编写任务
10 2 * * * /var/lib/mysql/mysql_db_backup.sh

# david @ Davids-Macbook-Pro in /tmp/mysql2/data [23:43:10]
$ crontab -l
10 2 * * * /var/lib/mysql/mysql_db_backup.sh