这玩意写的好真的可以事半功倍,有的大佬写的脚本真的强,各种一键操作。语法简单,只是一个小工具。但是如果不系统了解一下,用起来真的会哭的。

第六章-Linux下shell篇

shell编程

为什么要学习Shell编程?

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

Shell是什么?

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

image-20230204165247319

Linux 的 Shell 种类众多,一个系统可以存在多个 shell,可以通过 cat /etc/shells 命令查看系统中安装的 shell。

Bash 由于易用和免费,在日常工作中被广泛使用。同时,Bash 也是大多数Linux 系统默认的 Shell。

快速入门

以下所有操作都在/export/shell目录下进行,请提前创建该目录:

1
mkdir -p /export/shell

新建 /export/shell/hello.sh 文件

1
2
3
4
5
6
#!/bin/bash 
echo 'hello world'

#!/bin/sh是指此脚本使用/bin/sh来解释执行,
#! 是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell。注意,在shell脚本中除了第一行的#表示特殊格式外,其他地方的#一般表示注释。
echo命令用于向窗口输出文本。

解释器

Java需要虚拟机解释器, 同理 shell脚本也需要解析器,查看Linux系统支持的解释器:

1
2
3
4
5
6
7
[root@node1 shell]#cat /etc/shells
/bin/sh
/bin/bash
/sbin/nologin
/bin/dash
/bin/tcsh
/bin/csh

常见Shell解释器:

  • sh: 的全称是 Bourne shell,由 AT&T 公司的 Steve Bourne开发,为了纪念他,就用他的名字命名了。sh 是UNIX 上的标准 shell,很多 UNIX 版本都配有 sh。sh 是第一个流行的 Shell。
  • Bash:从名称可以看出是Bsh的升级版本,是著名的开源软件项目,目前大多数的Linux版本都使用Bash 作为默认的Shell程序,当运行Shell程序时,实际运行的是Bash程序
  • Csh:是因使用C语言的语法风格而得名,在用户的命令行交互界面上进行了很多改进,并增加了历史,别名,文件名替换,作业掏等功能,相比Bsh,Csh在更加适用为用户提供命令交互操作

在现代的 Linux上,sh 已经被 bash 代替,/bin/sh往往是指向/bin/bash的符号链接。

如果你希望查看当前 Linux 的默认 Shell,那么可以输出 SHELL 环境变量:

1
2
[root@node1 shell]# echo $SHELL 
/bin/bash

输出结果表明默认的 Shell 是 bash。

3种执行方式

方式一:sh执行脚本

sh执行,进入脚本的工作目录,然后使用对应的sh或bash来执行脚本,这种执行方式,脚本文件不需要具有可执行权限

1
2
3
[root@node1 ~]# cd /export/shell/ 
[root@node1 shell]#sh hello.sh
hello world

方式2:工作目录执行

执行脚本时,先进入到脚本所在的目录,然后使用 ./脚本方式执行,这种执行方式,必须保证脚本文件具有可执行权限

1
2
3
4
5
6
7
[root@node1 shell]# chmod +x hello.sh 
# 上面命令等价于:chmod 744 hello.sh
[root@node1 shell]# ll
总用量 4
-rwxr-xr-x. 1 root root 33 12月 31 20:18 hello.sh
[root@node1 shell]# ./hello.sh
hello world

方式三:绝对路径执行

绝对路径中执行,指的是直接从根目录/到脚本目录的绝对路径,这种执行方式,必须保证脚本文件具有可执行权限

1
2
[root@node1 ~]# /export/shell/hello.sh 
hello world

sh文件后缀无关,只是为了便于区分

3种方式区别:方式1和2一般是个人测试用,生产环境用方式3绝对路径

数据类型

字符串:
字符串是shell编程中最常用最有用的数据类型,字符串可以用单引号,也可以用双引号,也可以不用引号。建议使用双引号,因为双引号里可以有变量和特殊字符,可以按照变量和特殊字符去使用。
声明字符串类型的变量:

整数型:
在Shell中所有的变量默认都是字符串型。默认情况下,所有的数值都是不能进行运算的,如果想要进行数学运算,可以使用$((运算式))$[运算式]方式运算。

Shell的变量

变量的简介

shell变量是一种很“弱”的变量,默认情况下,一个变量保存一个串,shell不关心这个串是什么含义。所以若要进行数学运算,必须使用一些命令例如let、declare、expr、双括号等。

在shell中有3种变量:用户变量、环境变量、特殊变量,其中用户变量在编程过程中使用量最多,环境变量主要是在程序运行时需要设置,特殊变量在对参数判断和命令返回值判断时会使用,。

  1. 定义变量:变量=值

    a) 变量名称可以由字母、数字和下划线组成,但是不能以数字开头。

    b) 等号两侧不能有空格

    c) 变量名称一般习惯为大写

    d) 变量值中如果有空格,则需要使用单引号或双引号包含,如 test="hello world!"双引号括起来的内容”$”和反引号者都拥有特殊含义,而单引号括起来的内容都是普通字符

    e) 在变量值中,可以使用转义符”"。

    f) 不能使用bash里的关键字(可用help命令查看保留关键字)。

    h) 在 Bash中,变量的默认类型都是字符串型,如果要进行数值运算,则必须使用特殊命令。

用户变量

定义变量

在对变量赋于字符串值时,建议使用引号将其包裹。如果字符串中存在空格,请一定要使用单引号或双引号将整个内容包裹。注意:单引号里的内容原封不动的输出,双引号里有变量的调用则会调用变量。

1
[root@node1 shell]# username="nbchen"
访问变量

要对变量进行调用时,在变量名前加美元符号$

1
2
[root@node1 shell]# echo $username
nbchen

如果需要增加变量的值,那么可以进行变量值的叠加。不够变量需要用双引号包含"$变量名"${变量名}

1
2
3
4
5
6
7
[root@node1 ~]# usernmae="nbchen" 
[root@node1 ~]# echo $usernamedb.log #这种方式不可以
.log
[root@node1 ~]# echo "$username"db.log #可以
nbchendb.log
[root@node1 ~]# echo ${username}db.log #可以
nbchendb.log

测试脚本:test1.sh

1
2
3
4
#!/bin/bash 
string="I am shell"
num=5
echo "a=${num},string=${string}"

执行脚本,结果如下:

1
2
[root@node1 shell]# sh test1.sh 
a=,string=I am shell
变量的其他赋值方式
  1. 可以使用read关键字从键盘获取内容赋值给变量

  2. 并将命令的执行结果赋值给变量

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

测试脚本:test2.sh

1
2
3
4
5
6
7
8
#!/bin/bash 
echo "who are you?"
read name #从键盘获取变量的值
pwd_string=$(pwd) #将当前的绝对路径赋值给pwd_string变量
date_string=`date` #将当前时间赋值给date_string变量,注意这里使用的是反引号
echo "hello, $name"
echo $pwd_string
echo $date_string

执行脚本,结果如下:

1
2
3
4
5
6
[root@node1 shell]# sh test2.sh 
who are you?
nbchen
hello, nbchen
/export/data/shell
2020年 05月 10日 星期日 16:50:21 CST
只读变量

使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变(包括不能unset删除)。

下面的例子尝试更改只读变量,结果报错:

测试脚本:test3.sh

1
2
3
4
#!/bin/bash 
myUrl="http://www.google.com"
readonly myUrl
myUrl="http://www.runoob.com"

运行脚本,结果如下:

1
/bin/sh: NAME: This variable is read only.
删除变量

使用 unset 命令可以删除变量。语法:

1
unset variable_name

变量被删除后不能再次使用。unset 命令不能删除只读变量。

测试脚本:test4.sh

1
2
3
4
#!/bin/sh 
myUrl="http://www.runoob.com"
unset myUrl
echo $myUrl

以上实例执行将没有任何输出。

测试案例
1
2
3
4
案例1:定义变量A
案例2:撤销变量A
案例3:声明静态的变量B=2,不能unset
案例4:可把变量提升为全局环境变量,可供其他shell程序使用

image-20230919191956313

将结果返回

image-20230919192004017

环境变量

当shell程序启动时,都自动设置一组变量,这组变量就是环境变量。shell中的所有命令都可以使用这些变量,环境变量可以在/etc/profile中设置,环境变量的名字习惯上使用大写字母

系统环境变量:/etc/profile
用户环境变量: 用户家目录/bash_profile

1
2
系统变量:$HOME、$PWD、$SHELL、$USER等等
比如: echo $HOME 等等..
常见的环境变量

可以使用env命令查看所有的系统环境变量

image-20221231204954376

  • PATH 决定了shell将到哪些目录中寻找命令或程序
  • HOME 当前用户主目录
  • HISTSIZE 历史记录数
  • LOGNAME 当前用户的登录名
  • HOSTNAME 指主机的名称
  • SHELL 当前用户Shell类型
  • LANGUGE  语言相关的环境变量,多语言可以修改此环境变量
  • MAIL 当前用户的邮件存放目录
  • PS1 基本提示符,对于root用户是#,对于普通用户是$

image-20230919192013112

1
2
3
4
5
6
7
8
9
1) export 变量名=变量值 (功能描述:将shell变量输出为环境变量)
2) source 配置文件      (功能描述:让修改后的配置信息立即生效)
    注意:在输出JAVA_HOME 环境变量前,需要让其生效: source /etc/profile
3) echo $变量名         (功能描述:查询环境变量的值)

1) 在/etc/profile文件中定义TOMCAT_HOME环境变量
2) 查看环境变量TOMCAT_HOME的值
3) 在另外一个shell程序中使用 TOMCAT_HOME
参考JAVA_HOME PATH

image-20230919192017508

自定义环境变量

1)vi /etc/profile,在文件末尾加上要定义的环境变量,语法如下:
export 变量名=变量值

2)wq退出

3)source /etc/profile

4)输入 env 查看环境变量,是否有自己定义的环境变量。

5)环境变量的使用方式和普通变量是一样的:$环境变量名

特殊变量

我们可以在执行 Shell 脚本时,向脚本传递参数,这时候可以使用特变变量来获取参数,Shell常用的特殊变量如下:

变量 解释
$# 命令行参数的个数
$n n为数字,$0代表命令本身,$1-$9代表第一到第九个参数,十以上的参数,十以上的参数需要用大括号包含,如${10}
$0 当前程序的名称
$? 前一个命令或函数的返回码,如果这个变量的值为0,证明上一个命令正确执行;如果这个变量的值为非0(具体是哪个数,由命令自己来决定),则证明上一个命令执行不正确了
$* 以”参数1 参数2 。。。”形式保存所有参数
$@ 以”参数1” “参数2”。。。形式保存所有参数
$$ 本程序的(进程ID号)PID
$! 上一个命令的PID

$?返回[0-255]的状态码

测试脚本: test5.sh

以下实例我们向脚本传递三个参数,并分别输出,其中 $0 为执行的文件名:

1
2
3
4
5
6
7
8
#!/bin/bash 
echo "Shell 传递参数实例!";
echo "执行的文件名:$0";
echo "第一个参数为:$1";
echo "第二个参数为:$2";
echo "第三个参数为:$3";
echo "参数个数为:$#";
echo "传递的参数作为一个字符串显示*:$*";

执行脚本,结果如下:

1
2
3
4
5
6
7
8
9
[root@node1 shell]# chmod +x test5.sh 
[root@node1 shell]# ./test5.sh aaa bbb ccc
Shell 传递参数实例!
执行的文件名:./demo4.sh
第一个参数为:aaa
第二个参数为:bbb
第三个参数为:ccc
参数个数为:3
传递的参数作为一个字符串显示*:aaa bbb ccc

$*$@ 区别:

相同点:都是引用所有参数。
不同点:只有在双引号中体现出来。假设在脚本运行时写了三个参数 1、2、3,,则 " $* " 等价于 "1 2 3"(传递了一个参数),而 "$@" 等价于 "1" "2" "3"(传递了三个参数)。

案例:编写一个shell脚本 positionPara.sh ,在脚本中获取到命令行的各个参数信息。

image-20230919192024147

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

image-20230919192027865

字符串

字符串是shell编程中最常用最有用的数据类型(除了数字和字符串,也没啥其它类型好用了),字符串可以用单引号,也可以用双引号,也可以不用引号。

单引号

1
2
3
name='乔布斯'
str='我很崇拜$name'
echo $str

输出结果为:

1
我很崇拜$name

单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的,单引号字串中不能出现单独一个的单引号(对单引号使用转义符后也不行),但可成对出现,作为字符串拼接使用。

双引号

1
2
3
name="乔布斯"
str="我很崇拜$name"
echo $str

输出结果为:

1
我很崇拜 乔布斯
  1. 双引号里可以有变量

  2. 双引号里可以出现转义字符

拼接字符串

测试脚本:test.sh

1
2
3
4
5
6
#!/bin/bash
yourname="吴恩达"
wenhou_1="你好,$yourname ."
wenhou_2="你好,"$yourname" ."
wenhou_3="你好,\"$yourname\" ."
echo $wenhou_1 $wenhou_2 $wenhou_3

输出结果为:

1
你好,吴恩达 . 你好,吴恩达 . 你好,"吴恩达" 

获取字符串长度

测试脚本:test.sh

1
2
3
4
#!/bin/bash
string="jobs"
echo ${string} # 输出结果: jobs
echo ${#string} # 输出结果: 4

提取子字符串

测试脚本:test.sh

1
2
3
#!/bin/bash
string="敢于亮剑决不后退"
echo ${string:2:3} # 输出结果为: 亮剑决

追加

1
2
3
命令 > 文件 : 将命令成功结果覆盖指定文件
命令 >> 文件 :将命令成功结果追加指定文件
命令 &>> 文件:将命令失败结果追加指定文件

运算符

Shell 和其他编程一样,支持包括:算术、关系、布尔、字符串等运算符。原生 bash 不支持简单的数学运算,但是可以通过其他命令来实现,例如expr。expr 是一款表达式计算工具,使用它能完成表达式的求值操作。

1
2
3
4
1) "$((运算式))"或"$[运算式]" 不要有空格
2) expr m + n 注意expr运算符间要有空格
3) expr m - n
4) expr \* 乘, / 除, % 取余

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

image-20230919192035048

image-20230919192041932

条件判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[ condition ](注意condition前后要有空格)
#非空返回true,可使用$?验证(0为true,>1为false)

[ atguigu ] 返回true
[]          返回false
[condition] && echo OK || echo notok  条件满足,执行后面的语句

常用判断条件
1) 两个整数的比较
= 字符串比较
-lt 小于
-le 小于等于
-eq 等于
-gt 大于
-ge 大于等于
-ne 不等于
2) 按照文件权限进行判断
-r 有读的权限
-w 有写的权限
-x 有执行的权限
3) 按照文件类型进行判断
-f 文件存在并且是一个常规的文件
-e 文件存在
-d 文件存在并是一个目录

image-20230919192057211

流程控制

if 判断

1
2
3
4
5
6
7
8
9
10
11
12
if [ 条件判断式 ];then
    程序
fi
或者
if [ 条件判断式 ]
then
    程序
elif [ 条件判断式 ]
then
    程序
fi
注意事项:[ 条件判断式 ],中括号和条件判断式之间必须有空格;推荐使用第二种方式,第一种形式要多个分号

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

image-20230919192105493

案例:判断/media/cdrom文件是否存在,若不存在就去创建这个目录

1
2
3
4
5
6
#!/bin/bash 
DIR="/media/cdrom"
if [ ! -e $DIR ]
then
mkdir -p $DIR
fi

案例:判断是否成功了

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
read -p "Enter your age(1-100):" age
if [ $age -ge 18 ]
then
echo '已经成年!'
else
echo '未成年!'
fi

# read -p 可以不换行获取输入,更优雅
if和test结合使用

test 命令允许你做各种测试并在测试成功或失败时返回它的退出状态码(为0表示为真,为1表示为假)。使用这个状态码,可以让 Bash 对测试的结果做出反应。

1
2
3
4
test 命令的语法为:
test EXPRESSION

[ EXPRESSION ]

案例:

1
2
3
4
5
6
7
8
9
#!/bin/bash
num1=$[2*3]
num2=$[1+5]
if test $num1 -eq $num2
then
echo '两个数字相等!'
else
echo '两个数字不相等!'
fi
if多分支

案例:判断两个变量是否相等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash 
echo "请输入a的值:"
read a

echo "请输入b的值:"
read b
if [ $a -eq $b ]
then
echo "a 等于 b"
elif [ $a -gt $b ]
then
echo "a 大于 b"
elif [ $a -lt $b ]
then
echo "a 小于 b"
else
echo "没有符合的条件"
fi
if多条件

案例:输入成绩,判断成绩“优”“良”“中”

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
read -p "Enter your score(0-100):" n #-p参数表示给出提示信息
if [ $n -ge 85 ] && [ $n -le 100 ] ; then
echo "优"
elif [ $n -ge 70 ] && [ $n -le 84 ] ; then
echo "良"
elif [ $n -ge 60 ] && [ $n -le 69 ] ; then
echo "中"
else
echo "差"
fi

case选择

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

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

image-20230919192112582

案例2:提示输入1到4,与每一种模式进行匹配

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
#!/bin/bash
echo '输入 1 到 4 之间的数字:'

echo '你输入的数字为:'
read aNum
case $aNum in
1) echo '你选择了 1'
;;
2) echo '你选择了 2'
;;
3) echo '你选择了 3'
;;
4) echo '你选择了 4'
;;
*) echo '你没有输入 1 到 4 之间的数字'
;;
esac

# 穿透写法:
echo -n '你输入的数字为:' => -n 表示不换行类似 read -p
read aNum
case $aNum in
1|2|3|4) echo '你选择了 1~4的数字'
;;
*) echo '你没有输入 1 到 4 之间的数字'
;;
esac

for循环

1
2
3
4
5
6
7
8
9
10
方式一:
for 变量 in 值1 值2 值3… => 注意,是空格,如果是逗号,会被当成一个整体
do
    程序
done
方式二:
for (( 初始值;循环控制条件;变量变化 ))
do
    程序
done

案例1 :打印命令行输入的参数
案例2 :从1加到100的值输出显示 [这里可以看出$*$@ 的区别] 一般用$@

image-20230919192118161

S=$((s+i))

可以写做:

let s = s+i

还可以自增: let num++

案例3:打印/root目录下所有文件的名字

1
2
3
4
5
#!/bin/bash  
for file in $(ls /root)
do
echo $file
done

while循环

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

案例1 :从命令行输入一个数n,统计从 1+..+ n 的值是多少?

image-20230919192125652

无限循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
while :
do
command
done

或者
while true
do
command
done

或者
for (( ; ; ))

循环中可以用 sleep 1 睡眠1s

跳出循环

在循环过程中,有时候需要在未达到循环结束条件时强制跳出循环,Shell使用两个命令来实现该功能:break和continue。

break

break命令允许跳出所有循环(终止执行后面的所有循环)。

下面的例子中,脚本进入死循环直至用户输入数字大于5。要跳出这个循环,返回到shell提示符下,需要使用break命令。

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

while :
do
echo -n "输入 1 到 5 之间的数字:"

read aNum

case $aNum in
1|2|3|4|5) echo "你输入的数字为 $aNum!"
;;
*) echo "你输入的数字不是 1 到 5 之间的! 游戏结束"
break
;;
esac
done

执行以上代码,输出结果为:

1
2
3
4
输入 1 到 5 之间的数字:3
你输入的数字为 3!
输入 1 到 5 之间的数字:7
你输入的数字不是 1 到 5 之间的! 游戏结束

exit是终止进程

continue

continue命令与break命令类似,只有一点差别,它不会跳出所有循环,仅仅跳出当前循环。

对上面的例子进行修改:

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

while :

do

echo -n "输入 1 到 5 之间的数字: "
read aNum
case $aNum in
1|2|3|4|5) echo "你输入的数字为 $aNum!"
;;
*) echo "你输入的数字不是 1 到 5 之间的!"
continue
echo "游戏结束"
;;
esac
done

运行代码发现,当输入大于5的数字时,该例中的循环不会结束,语句 echo “游戏结束” 永远不会被执行。

read读取控制台输入

1
2
3
4
5
6
read(选项)(参数)
选项
    -p:指定读取值时的提示
    -t:指定读取值时等待的时间(秒),如果没有在指定的时间内输入,就不再等待了。。
参数
    变量:指定读取值的变量名

image-20230919192131402

函数

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

# 系统函数
basename :返回完整路径最后 / 的部分,常用于获取文件名
    basename [pathname] [suffix]
    basename [string] [suffix]
basename命令会删掉所有的前缀包括最后一个('/')字符,然后将字符串显示出来。
suffix为后缀,如果suffix被指定了,basename会将pathname或string中的suffix去掉。

image-20230919192136196

1
2
dirname :返回完整路径最后 / 的前面的部分,常用于返回路径部分
    dirname 文件绝对路径 (功能描述:从给定的包含绝对路径的文件名中去除文件名(非目录的部分),然后返回剩下的路径(目录的部分))

image-20230919192140919

1
2
3
4
5
6
7
8
9
# 自定义函数
[ function ] funname[()]
{
    Action;
    [return int;] => return的是状态码,并不是执行的结果,输出变量可以用echo $var
}
调用直接写函数名:funname [值]

案例1:计算输入两个参数的和, getSum

image-20230919192145635

可以带function fun() 定义,也可以直接fun() 定义,不带任何参数。

案例1:定义一个函数并进行调用

1
2
3
4
5
6
7
8
#!/bin/bash
demoFun(){
   echo "这是我的第一个 shell 函数!"
}

echo "-----函数开始执行-----"
demoFun
echo "-----函数执行完毕-----"

输出:

1
2
3
-----函数开始执行-----
这是我的第一个 shell 函数!
-----函数执行完毕-----

注意:所有函数在使用前必须定义。这意味着必须将函数放在脚本开始部分,直至shell解释器首次发现它时,才可以使用。调用函数仅使用其函数名即可。

案例2:定义一个带有return语句的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
funWithReturn(){

echo "这个函数会对输入的两个数字进行相加运算..."

echo "输入第一个数字: "
read aNum

echo "输入第二个数字: "

read anotherNum
echo "两个数字分别为 $aNum 和 $anotherNum !"

echo "两个数的和为:$(($aNum+$anotherNum))"
return 100
}

funWithReturn

echo "退出状态码: $? !"

输出:

1
2
3
4
5
6
7
8
这个函数会对输入的两个数字进行相加运算...
输入第一个数字:
1000
输入第二个数字:
2000
两个数字分别为 1000 和 2000 !
两个数的和为:3000
退出状态码: 100 !

案例3:获取函数的运算结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash
funWithReturn(){
s=0
#求1-n的和,$1为传入的第一个参数
for(( i=1;i<=$1;i=i+1))
do
s=$(($s+$i))
done
#返回计算结果
echo $s
return 0;
}
#调用函数,并传入参数,求1-100的和
#result用户获取函数的返回结果,也就是echo后边的内容
result=$(funWithReturn 100)
echo "1-$1的和:$result"
#状态码为0表示正常退出,非0表示不正常退出(状态码范围:0-255)
echo "退出状态码: $? !"

image-20221231223440618

函数带参数

在Shell中,调用函数时可以向其传递参数。在函数体内部,通过 的形式来获取参数的值,例如,1表示第一个参数,$2表示第二个参数…

带参数的函数示例:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
funWithParam(){
echo "第一个参数为 $1 !"
echo "第二个参数为 $2 !"
echo "第十个参数为 $10 !"
echo "第十个参数为 ${10} !"
echo "第十一个参数为 ${11} !"
echo "参数总数有 $# 个!"
echo "作为一个字符串输出所有参数 $* !"
}

funWithParam 1 2 3 4 5 6 7 8 9 34 73

输出为:

1
2
3
4
5
6
7
第一个参数为 1 !
第二个参数为 2 !
第十个参数为 10 !
第十个参数为 34 !
第十一个参数为 73 !
参数总数有 11 个!
作为一个字符串输出所有参数 1 2 3 4 5 6 7 8 9 34 73 !

数组

数组中可以存放多个值。Bash Shell 只支持一维数组(不支持多维数组),初始化时不需要定义数组大小(与 PHP 类似)。

与大部分编程语言类似,数组元素的下标由0开始。

Shell 数组用括号来表示,元素用”空格”符号分割开,语法格式如下:

1
array_name=(value1 ... valuen)

测试脚本:test.sh

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/bash

my_array=("A" "B" "C" "D")

#我们也可以使用下标来定义数组:
array_name[0]=value0
array_name[1]=value1
array_name[2]=value2

# 读取数组
echo "第一个元素为: ${my_array[0]}"
echo "第二个元素为: ${my_array[1]}"
echo "第三个元素为: ${my_array[2]}"
echo "第四个元素为: ${my_array[3]}"

# 获取数组所有元素
echo "数组的元素为: ${my_array[*]}"
echo "数组的元素为: ${my_array[@]}"

*和@的区别:如果遍历的变量用""括起来是,@会正常输出,而*会当成整体输出

# 获取数组的长度
echo "数组元素个数为: ${#my_array[*]}"
echo "数组元素个数为: ${#my_array[@]}"

# 遍历数组
方式1:
my_arr=(AA BB CC)

for var in ${my_arr[*]}
do
echo $var
done

方式2:
my_arr=(AA BB CC)
my_arr_num=${#my_arr[*]}
for((i=0;i<my_arr_num;i++));
do
echo "-----------------------------"
echo ${my_arr[$i]}
done

select

select表达式是bash的一种扩展应用,擅长于交互式场合。用户可以从一组不同的值中进行选择:

1
2
3
4
5
select var in ... ;
do
 commond
done
.... now $var can be used ...

注意:select 是个无限循环,因此要记住用 break 命令退出循环,或用exit 命令终止脚本

测试脚本:test.sh

1
2
3
4
5
6
7
8
#!/bin/bash
echo "你喜欢那个操作系统?"
PS3="请输入你的选择:"
select var in "Linux" "Mac" "Windows" "Other"
do
break;
done
echo "你选择了 $var 系统"

这里PS3作为select语句的shell界面提示符,注意:PS3一定要定义在select语句的前面

该脚本的运行结果如下:

1
2
3
4
5
6
7
What is your favourite OS?
1) Linux
2) Gnu Hurd
3) Free BSD
4) Other
Please enter your choice:3
You have selected Free BSD

图片效果:

image-20230103212909917

select和case的综合练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash
echo "你想学习什么语言?"
PS3="请输入你的选择:" # 设置提示字符串
select var in java c++ shell python
do
# 嵌套具体的操作,匹配你的选择
case $var in
"java")
echo "恭喜你选择成功.java最牛"
;;
"c++")
echo "驱动开发 网络优化 go 语言"
;;
"shell")
echo "运维必会"
;;
python)
echo "人工智能"
esac
break # 如果这里没有break 将会进行无限循环
done
echo "你选择的是:$var"

加载其它文件的变量

和其他语言一样,Shell 也可以包含外部脚本。这样可以很方便的封装一些公用的代码作为一个独立的文件。

1
2
3
4
. filename   # 注意点号(.)和文件名中间有一空格

source filename
# 路径最好用绝对路径,不建议用相对路径

案例:定义两个文件 demo1.sh和demo2.sh,在test1中定义一个变量arr=(java c++ shell),在demo2中对arr进行循环打印输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
vim demo1.sh
#!/bin/bash
my_arr=(AA BB CC)

vim demo2.sh
#!/bin/bash
source ./test1.sh # 加载test1.sh 的文件内容 导入多个,以最后的为准
for var in ${my_arr[*]}
do
echo $var
done

# 执行demo2
sh demo2.sh

好处 :

1. 数据源和业务处理分离

2. 复用代码扩展性更强

Shell编程综合案例

需求分析

1)每天凌晨 2:10 备份 数据库 atguiguDB 到 /data/backup/db

2)备份开始和备份结束能够给出相应的提示信息

3)备份后的文件要求以备份时间为文件名,并打包成 .tar.gz 的形式,比如:

2018-03-12_230201.tar.gz

4) 在备份的同时,检查是否有10天前备份的数据库文件,如果有就将其删除。

image-20230919192200596

思路:

image-20230919192204163

1
2
# cd /usr/sbin
# vim mysql_db_backup.sh

脚本内容

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
#!/bin/bash
#完成数据库的定时备份
#备份的路径
BACKUP=/data/backup/db
#当前的时间作为文件名
DATETIME=$(date +%Y_%m_%d_%H%M%S)
#可以输出变量调试
#echo $DATETIME
#echo ${DATETIME} # 用中括号包裹起来也可以输出

echo "=========开始备份======="
echo "=========备份的路径是: $BACKUP/$DATETIME.tar.gz======="

#主机
HOST=localhost
#用户名
DB_USER=root
#密码
DB_PWD=root
#备份数据库名
DATABASE=atguiguDB
#创建备份的路径
#如果备份的路径文件夹存在,就使用,否则就创建
[ ! -d "$BACKUP/$DATETIME" ] && mkdir -p "$BACKUP/$DATETIME"
#执行mysql的备份数据库的指令
mysqldump -u${DB_USER} -p${DB_PWD} --host=$HOST $DATABASE | gzip > $BACKUP/$DATETIME/$DATETIME.sql.gz
#打包备份文件
cd $BACKUP
tar -zcvf $DATETIME.tar.gz $DATETIME
#删除临时目录
rm -rf $BACKUP/$DATETIME
#删除10天前的备份文件
find $BACKUP -mtime +10 -name "*.tar.gz" -exec rm -rf {} \;
echo "=========备份文件成功==========="

添加到定时任务调度

1
2
3
$ crontab -e 
编写任务命令: 
10 2 * * * /usr/sbin/mysql_db_backup.sh

image-20230919192210073

shell脚本部署jar

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
#!/bin/bash

# jar path
APP_NAME=/apps/xxl-job/xxl-job-admin-2.3.1-SNAPSHOT.jar

#log path
LOG_NAME=/apps/logs/xxl-job/xxl-job-admin.log

#kill process
pid=`ps -ef|grep $APP_NAME | grep -v grep | awk '{print $2}'`
kill -9 $pid

echo "$pid kill success"

sleep 2

#check jar exist, yes? start and open log file
if test -e $APP_NAME
then
echo "app exist, start"

#start jar
nohup java -Xms512m -Xmx512m -Xmn128m -Xss256K -XX:PermSize=64m -jar $APP_NAME >$LOG_NAME 2>&1 &

#start success, open log file
tail -f $LOG_NAME

#file not exist, prompt!
else
echo "$APP_NAME is not exist, please check it out"
fi

清理日志

清除当前目录下的所有日志文件

1
find . -name "*.txt" -type f -print -exec rm -rf {} \;

更新于2020年12月11日14:02:40

1
2
3
4
这几天脚本一直跑失败了。手动执行可以,自动就不行,一直不知道为什么
群里老大和我说,服务器磁盘满了。。。我心里咯噔一下
莫非是因为磁盘满了。所以任务执行失败了。
裂开

查询FullLink目录及子目录的大文件

1
2
3
4
5
6
7
find . -type f -size +800M
我们仅仅能看到超过800M大小的文件的文件名称,但是对文件的信息(例如,文件大小、文件属性)一无所知,那么能否更详细显示一些文件属性或信息呢
find . -type f -size +800M  -print0 | xargs -0 ls -l
当我们只需要查找超过800M大小文件,并显示查找出来文件的具体大小,可以使用下面命令
find . -type f -size +800M  -print0 | xargs -0 du -h
如果你还需要对查找结果按照文件大小做一个排序,那么可以使用下面命令
find . -type f -size +800M  -print0 | xargs -0 du -h | sort -nr

CROND任务调度

任务调度原理:

image-20230919192216235

任务调度:是指系统在某个时间执行的特定的命令或程序。

任务调度分类:

  • 1.系统工作:有些重要的工作必须周而复始地执行。如病毒扫描等
  • 2.个别用户工作:个别用户可能希望执行某些程序,比如对mysql数据库的备份。

crontab 进行 定时任务的设置。

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
crontab -e: 编辑crontab定时任务
crontab –r:终止任务调度(删除当前用户所有的crontab任务)。
crontab –l:列出当前有那些任务调度(查询crontab任务)
service crond restart [重启任务调度]

简单任务入门:
设置任务调度文件:/etc/crontab
设置个人任务调度。执行crontab –e命令。
接着输入任务到调度文件
如:*/1 * * * * ls –l /etc/ > /tmp/to.txt
意思说每小时的每分钟执行ls –l /etc/ > /tmp/to.txt命令

占位符详解:
第一个"*"    一小时当中的第几分钟  0-59
第二个"*"    一天当中的第几小时    0-23
第三个"*"    一个月当中的第几天    1-31
第四个"*"    一年当中的第几月      1-12
第五个"*"    一周当中的星期几      0-7(0和7都代表星期日)

特殊符号说明
*      代表任何时间。          比如第一个“*”就代表一小时中每分钟都执行一次的意思。
,     代表不连续的时间。      比如“0 8,12,16 * * * 命令”,就代表在每天的8点0分,12点0分,16点0分都执行一次命令
-      代表连续的时间范围。    比如“0 5 * * 1-6命令”,代表在周一到周六的凌晨5点0分执行命令
*/n    代表每隔多久执行一次。  比如“*/10 * * * * 命令”,代表每隔10分钟就执行一遍命令

特定时间执行任务案例
45   22   *    * *   命令    在22点45分执行命令
0    17   *    * 1   命令    每周1 的17点0分执行命令
0    5    1,15 * *   命令    每月1号和15号的凌晨5点0分执行命令
40   4    *    * 1-5 命令    每周一到周五的凌晨4点40分执行命令
*/10 4    *    * *   命令    每天的凌晨4点,每隔10分钟执行一次命令
0    0    1,15 * 1   命令    每月1号和15号,每周1的0点0分都会执行命令。注意:星期几和几号最好不要同时出现,因为他们定义的都是天。非常容易让管理员混乱。

案例1:每隔1分钟,就将当前的日期信息,追加到 /tmp/mydate 文件中

1
2
3
4
5
# 1. 在/home/test下创建脚本 myTask.sh。 
     编写内容: date >> /tmp/mydate
# 2. 授权脚本: chmod 744 myTask.sh  (颜色由普通白色变可执行文件绿色)
# 3. 任务调度: crontab -e 
     编写任务调度命令: */1 * * * * /home/test/myTask.sh

image-20230919192223515

案例2:每隔1分钟, 将当前日期和日历都追加到 /tmp/mycal 文件中

1
2
3
4
5
6
7
# 1. 在/home/test下创建脚本 myTask2.sh。
     编写内容:
            date >> /tmp/mycal
            cal >> /tmp/mycal
# 2. 授权脚本: chmod 744 myTask2.sh  (颜色由普通白色变可执行文件绿色)
# 3. 任务调度: crontab -e
     编写任务调度命令: */1 * * * * /home/test/myTask2.sh

image-20230919192227633

案例3: 每天凌晨2:00 将mysql数据库 testdb ,备份到文件中。

1
2
3
4
5
# 1. 在/home/test下创建脚本 myTask3.sh。
     编写内容: /usr/local/mysql/bin/mysqldump -u root -proot testdb > /tmp/mydb.bak
# 2. 授权脚本: chmod 744 myTask3.sh  (颜色由普通白色变可执行文件绿色)
# 3. 任务调度: crontab -e
     编写任务调度命令: 0 2 * * * /home/test/myTask3.sh

猜字游戏

游戏规则为:程序内置一个1到100 之间的数字作为猜测的结果,由用户猜测此数字。用户每猜测一次,由系统提示猜测结果:大了、小了或者猜对了;直到用户猜对结果,则提示游戏结束。

创建脚本:guess_number.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
#生成100以内的随机数 提示用户猜测 猜对为止
#random 系统自带,值为0-32767任意数
#随机数1-100
num=$[RANDOM%100+1]
#read 提示用户猜数字
#if判断
while :
do
read -p "计算机生成了一个 1‐100 的随机数,你猜: " cai
if [ $cai -eq $num ]
then
echo "恭喜,猜对了"
exit
elif [ $cai -gt $num ]
then
echo "不巧,猜大了"
else
echo "不巧,猜小了"
fi
done

一键安装jdk

在做这个案例之前,将之前安装的JDK全部删除:

1.删除jdk安装目录

1
rm -rf /export/server/jdk1.8.0_241/ 

2.删除/etc/profile中的JDK环境变量配置内容

image-20230919192233900

3.让修改生效

1
source /etc/profile

4.安装脚本jdk_install.sh

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
#!/bin/bash
# 1.提示安装jdk
echo "现在开始安装jdk"
sleep 1 # 休眠1秒钟
# 2.删除centos自带的jdk
oldjava=$(rpm -qa | grep java )
for old in ${oldjava}
do
   # echo $old
  rpm -e --nodeps $old
done
# 3.创建安装目录/export/server, 进入安装目录

java_path="/export/server"

if [ ! -d $java_path ]
then
   mkdir -p $java_path
fi

#4:解压jdk安装包
tar -xvf /export/software/jdk-8u241-linux-x64.tar.gz -C $java_path

# 6.设置环境变量
JAVA_HOME="/export/server/jdk1.8.0_241"

grep "JAVA_HOME" /etc/profile
if [ $? -ne 0 ]
then
# JAVA_HOME
echo "--------------JAVA_HOME------------------"
echo 'export JAVA_HOME=/export/server/jdk1.8.0_241' >> /etc/profile
# PATH
echo "--------------PATH------------------------"
echo 'export PATH=:$JAVA_HOME/bin:$PATH' >> /etc/profile
fi
# 7.加载环境变量
source /etc/profile
# 8.提示用户安装成功,查看jdk安装版本
echo "恭喜您,jdk安装成功!"
java -version

执行该脚本

1
2
chmod +x jdk_install.sh
./jdk_install.sh

image-20230103215833171

数据库定时备份

1)每天凌晨1:15备份 数据库 itcast_shop到 /export/data/db目录

2)备份开始和备份结束能够给出相应的提示信息

3)备份后的文件要求以备份时间为文件名,并打包成 .tar.gz 的形式,比如:

2020-05-12_021501.tar.gz

4)在备份的同时,检查是否有10天前备份的数据库文件,如果有就将其删除

脚本mysqldump_demo.sh

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
#!/bin/bash
#完成数据库的定时备份
#备份的路径
BACKUP=/export/data/db
#当前时间作为文件名
DATETIME=$(date +%Y_%m_%d_%H%M%S)
#可以通过输出变量来调试
#echo ${DATETIME}

echo "---------------------开始备份数据库---------------------"
echo "---------------------备份的路径是$BACKUP/$DATETIME.tar.gz---------------------"
#主机ip地址
HOST=192.168.88.161
#数据库用户名
DB_USER=root
#数据库密码
DB_PWD=123456
#数据库名
DATABASE=itcast_shop
#创建备份路径
#如果备份的文件夹路径存在的话,就直接使用,否则就创建

[ ! -d "${BACKUP}/${DATETIME}" ] && mkdir -p "${BACKUP}/${DATETIME}"

#执行mysql的备份数据库的指令

/export/server/mysql-5.7.29/bin/mysqldump -u${DB_USER} -p${DB_PWD} --host=${HOST} ${DATABASE} > ${BACKUP}/${DATETIME}/${DATETIME}.sql

#打包备份文件
cd ${BACKUP}
tar -czvf ${DATETIME}.tar.gz ${DATETIME}
#删除临时目录
rm -rf ${BACKUP}/${DATETIME}
#删除10天前的备份文件
find ${BACKUP} -mtime +10 -name "*.tar.gz" -exec rm -rf {} \;
echo "-------------------------备份成功-------------------------"

代码说明:

  1. 代码中HOS、DB_USER、DB_PWD这三个变量的值要根据自己的情况来设置

  2. mysqldump就是系统内置的用来备份数据库的工具

  3. 语句\find ${BACKUP} -mtime +10 -name "*.tar.gz" -exec rm -rf {} \; 解释:

1)find命令只是来查找匹配的文件,如果查到文件之后还需要进一步的操作,则需要添加-exec参数,{}表示find查询出来的文件名字。

2)-exec 参数后面跟的是要执行的command命令,它的终止是以;为结束标志的,所以这句命令后面的分号是不可缺少的,考虑到各个系统中分号会有不同的意义,所以前面加反斜杠。

3)-mtime 文件状态修改时间 ,-mtime +10:表示10天前的文件

导入脚本:

将itcast_shop.sql提前导入到数据库中

1.选择导入sql标本

image-20230103220233388

2.指定要导入的脚本文件

image-20230103220303165

执行完之后,点击完成

3.刷新

image-20230103220321774

运行脚本:

1
2
3
4
5
6
7
8
9
10
11
[root@node1 shell]# vim mysqldump_demo.sh 
[root@node1 shell]# chmod +x mysqldump_demo.sh
[root@node1 shell]# ./mysqldump_demo.sh
2020_05_12_102918
2020_05_12_102918
---------------------开始备份数据库---------------------
---------------------备份的路径是/export/data/db/2020_05_12_102918.tar.gz---------------------
mysqldump: [Warning] Using a password on the command line interface can be insecure.
2020_05_12_102918/
2020_05_12_102918/2020_05_12_102918.sql.gz
-------------------------备份成功-------------------------

结果查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1:进入数据库备份目录
[root@node1 shell]# cd /export/data/db/
[root@node1 db]# ls
2020_05_12_102918.tar.gz

2:解压备份压缩包
[root@node1 db]# tar -zxvf 2020_05_12_102918.tar.gz
2020_05_12_102918/

3:进入解压后目录
[root@node1 db]# cd 2020_05_12_102918/
[root@node1 2020_05_12_102918]# ls
2020_05_12_102918.sql

4:查看sql脚本内容
[root@node1 2020_05_12_102918]# vim 2020_05_12_102918.sql

查看sql脚本内容,发现备份成功!

image-20230103220401388

定时执行:

这里配置定时任务,需要使用Linux的定时工具crontab,crontab语法如下:

image-20230103220423129

在以上各个字段中,还可以使用以下特殊字符:

星号(*):代表所有可能的值,例如month字段如果是星号,则表示在满足其它字段的制约条件后每月都执行该命令操作。

“?”字符仅被用于天(月)和天(星期)两个子表达式,表示不指定值

逗号(,):可以用逗号隔开的值指定一个列表范围,例如,“1,2,5,7,8,9”

中杠(-):可以用整数之间的中杠表示一个整数范围,例如“2-6”表示“2,3,4,5,6”

正斜线(/):可以用正斜线指定时间的间隔频率,例如“0-23/2”表示每两小时执行一次。同时正斜线可以和星号一起使用,例如*/10,如果用在minute字段,表示每十分钟执行一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
分(0~59) 
时(0~23)
日(0~31,但是你需要考虑你月的天数)
月(1~12)
周(0~6 0=SUN 或 SUN,MON,TUE,WED,THU,FRI,SAT)

"* * * * *" #每隔一分钟触发
"15 * ? * *" #每小时的第15分触发
"15 10 ? * *" #每天上午10:15触发
"15 10 * * ?" #每天上午10:15触发
"* 14 * * ?" #在每天下午2点到下午2:59期间的每1分钟触发
"0/5 14 * * ?" #在每天下午2点到下午2:55期间的每5分钟触发
"0/5 14,18 * * ?" #在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
"0-5 14 * * ?" #在每天下午2点到下午2:05期间的每1分钟触发
"10,44 14 ? 3 WED" #每年三月的星期三的下午2:10和2:44触发
  1. 命令行输入crontab -e 进入编辑模式

  2. 编辑,写入以下内容

1
15 1 * * * /export/data/shell/mysqldump_demo.sh
  1. wq保存退出即可

  2. 配置定时任务调度成功!这样的话,每天的凌晨 1:15就会自动的备份数据库

image-20230103220514391