学习shell脚本

什么是shell脚本

什么是shell脚本呢(shell script,程序化脚本)呢?就字面上的意义,我们将它分为两部分就是在命令行下面让我们与系统沟通的一个工具接口。那么【script】是啥?字面意思就是【脚本、剧本】的意思。

整句话来说shell脚本就是利用shell的功能所写的一个【程序】,这个程序是使用纯文本文件,将一些shell的语法与命令(含外部命令),写在里面,搭配正则表达式、管道命令与数据流重定向等功能,达到我们所想要处理的目的。

shell脚本可以简单地被看成批处理文件,也可以被说成是一个程序文件,且这个程序语言由于都是利用shell与相关工具命令,所以不需要编译即可执行

为什么要学习shell脚本

为什么要学习shell脚本呢,如果你不从事IT工作,只想要【会用】Linux而已,那么不需要学shell脚本也无所谓。但是,如果你是真的想要玩明白Linux,那么shell脚本肯定要学,因为shell脚本给我带来以下好处:

  1. ==自动化管理:==Shell脚本可以帮助管理员自动化管理一些重复性的任务,例如备份数据、批量修改文件、自动化部署等,从而提高工作效率和减少错误。
  2. ==系统管理:==Shell脚本可以帮助管理员进行系统管理,例如监测系统性能、管理进程、设置定时任务等。
  3. ==脚本编写:==Shell脚本是一种非常简单易懂的编程语言,掌握Shell脚本可以帮助开发人员快速编写一些小型脚本程序,例如数据处理、文本解析等。
  4. ==维护脚本:==在Linux系统中,很多应用程序都是通过脚本来启动和停止的,例如Apache、MySQL等,因此掌握Shell脚本可以帮助管理员更好地维护这些应用程序。
  5. ==调试程序:==Shell脚本是一种非常容易调试的编程语言,可以帮助开发人员快速定位程序中的错误并进行修复。

第一个脚本编写与执行

shell脚本其实就是纯文本文件,我们可以编辑这个文件,然后让这个文件来帮我们一次执行多个命令,或是利用一些运算与逻辑判断来帮我们完成某些功能。在shell脚本的编写中需要注意下面的事项:

  1. 命令是从上而下、从左向右地分析执行
  2. 命令、选项与参数间的多个空格都会被忽略掉
  3. 空白行也将被忽略掉,并且【TAB】按键所产生的空白行视为空格键
  4. 如果读取到一个Enter符号,就尝试开始执行该行(或该串)命令
  5. 至于如果一行内容太多,则可以用【\Enter】来扩展至下一行
  6. 【#】可作为注释,任何加在#后面的数据将全部视为注释文字

这样一来,我写的脚本程序,就会被一行一行地执行,现在我们假设你写的这个程序文件名是/home/csq/shell.sh 那如何执行这个文件呢?

  • 直接命令行执行:shell.sh 文件必须要具备可读与可执行的权限(rx),然后:
    • 绝对路径:使用/home/csq/shell.sh 来执行命令
    • 相对路径:假设工作目录在/home/csq下,则使用【./shell.sh】来执行
    • 变量【PATH】功能:通过【bash shell.sh】或【sh shell.sh】来执行

重点就是让shell.sh文件必须要具备可读与可执行的权限才行

那么【sh shell.sh】也可以执行?这是因为/bin/sh其实就是/bin/bash(链接文件),使用sh shell.sh就是告诉系统,我想要直接以bash的功能来执行shell.sh 这个文件内相关命令的意思。所以说只要你shell.sh具有可读可执行的权限,就能用 sh 的参数。

image-20230427103722080

学过语言的都知道一开始学都是从输出【Hello World】开始的,我们可以编一个输出Hello world的shell脚本:

[root@chenshiren ~]# mkdir /tmp/shelldir ; cd /tmp/shelldir/
[root@chenshiren shelldir]# vim hello-world.sh 
#!/bin/bash
# 说明:
# 在屏幕上输出hello,world!
# 时间: 2024/3
PATH=$PATH:/tmp/shelldir
export PATH
echo -e "Hello World! \a \r"
exit=0
  1. 第一行 #!/bin/bash 在声明这个使用的shell名称

因为我们使用的是bash,所以,必须要以【#!/bin/bash】来声明这个文件内使用bash的语法。这样【#!】开头的行被称为shebang行。那么当这个程序被执行时,它就能够加载bash的相关环境配置文件(这个文件一般来说是~/.bashrc),并且执行bash来使我们下面的命令能够执行。这很重要,在很多错误的情况中,如果没有设置好这行,那么该程序可能会无法执行,因为系统可能无法判断该程序需要声明shell来执行。

  1. 程序说明内容

整个脚本当中,除了第一行【#!】是用来声明shell之外,其他的 # 都是【注释】用途。所以上面的程序当中,第二行以下就是用来说明整个程序的基本数据。一般来说,建议你一定要养成习惯,说明该脚本的:(1)内容与功能(2)版本信息(3)作者联络方式(4)建文件日期(5)历史记录等等,这样有助于未来程序的改写与调试。(建议大家注释使用英文注释主要原因是为了兼容性和可移植性),下面案例的注释部分我写成中文方便阅读

  1. 主要环境变量的声明

建议务必要将一些重要的环境变量设置好,PATH与LANG(如果有使用到输出相关的信息时)是当中最重要的。如此一来,我们这个程序在进行时,可以直接执行一些外部命令,而不必写绝对路径

  1. 主程序部分

将主要的程序写好即可,在这个例子当中,就是echo 那一行

  1. 执行结果告知(定义返回值)

一个命令的成功与否,可以使用$?这个变量来观察?那么我们也可以利用exit这个命令来让程序中断,并且返回一个数值给系统。在此例中,使用exit 0,代表退出脚本并且返回一个0给系统,所以我执行完这个脚本后,若接着执行echo $? 则得到0的值。

接下来执行一下写的脚本

[root@chenshiren shelldir]# chmod +x hello-world.sh ; ./hello-world.sh 
Hello World! 

简单的shell脚本练习

简单案例

==交互式脚本:变量内容由用户决定

很多时候我们需要用户输入一些内容,好让程序可以顺利运行。大家应该都有安装过软件的经验,安装的时候,它不是会问你【安装到哪个目录去】吗?那个让用户输入数据的操作,就是让用户输入变量内容。

请你以read命令的用途,编写一个脚本,它可以让用户输入:1. first name 2. last name 最后在屏幕上显示:【Your full name is】的内容:

[root@localhost shelldir]# vim showname.sh
#!/bin/bash
# 说明:
# 用户输入字节的姓和全名,程序会输出用户的姓和全名是什么
# 时间:2024/3
PATH=$PATH:/tmp/shelldir
export PATH
read -p "输入姓名:" xm
read -p "输入全名:" qm
echo -e "你姓:$xm \n全名叫:$qm \a"

执行结果如下,你能够发现用户自己输入的变量可以让程序所使用,并且将它显示到屏幕上。

[root@chenshiren shelldir]# chmod +x showname.sh ; ./showname.sh  
输入姓名:c
输入全名:csq
你姓:c 
全名叫:csq 

随日期变化:利用date建立文件

假设我的服务器内有数据库,数据库每天的数据都不太一样,因此当我备份时,我希望每天的数据备份成不同的文件名,这样才能让旧的数据也能够保存下来不被覆盖。应该怎么做呢?

假设我想要建立三个空文件(通过touch),文件名最开头由用户输入决定,假如用户输入filename,而今天的日期是2023/04/27,我想要以前天、昨天、今天的日期来建立这些文件,即filename_2023/04/25、filename_2023/04/26、filename_2023/04/27,那该如何是好?

[root@localhost shelldir]# vim create_3_filename.sh
#!/bin/bash
# 说明:根据用户输入文件名字,程序自动创建3个文件以前天昨天今天来命名
# 时间:2024/3
PATH=$PATH:/tmp/shelldir
export PATH
unset fileuser
unset filename
echo "输入文件名我将使用touch创建三个文件"
read -p "输入文件名:" fileuser

filename=${fileuser:-"filename"}  # 如果没有输入文件名默认文件名会设置为filename

date1=$(date --date="2 days ago" +%Y%m%d)  # 设置时间
date2=$(date --date="1 days ago" +%Y%m%d)
date3=$(date +%Y%m%d)

file1=${filename}_${date1}  # 设置文件名
file2=${filename}_${date2}
file3=${filename}_${date3}
touch "${file1}"           # 创建文件
touch "${file2}"
touch "${file3}"

ls -al "${PWD}/${fileuser}"* # 打印创建的文件
echo -e "\a"

执行结果如下

[root@chenshiren shelldir]# chmod +x create_3_filename.sh ; ./create_3_filename.sh
输入文件名我将使用touch创建三个文件
输入文件名:csq
-rw-r--r--. 1 root root 0  315 15:18 /tmp/shelldir/csq_20240313
-rw-r--r--. 1 root root 0  315 15:18 /tmp/shelldir/csq_20240314
-rw-r--r--. 1 root root 0  315 15:18 /tmp/shelldir/csq_20240315

[root@chenshiren shelldir]# sh create_3_filename.sh 
输入文件名我将使用touch创建三个文件
输入文件名:
-rwxr-xr-x. 1 root root 790  315 15:17 /tmp/shelldir/create_3_filename.sh
-rw-r--r--. 1 root root   0  315 15:18 /tmp/shelldir/csq_20240313
-rw-r--r--. 1 root root   0  315 15:18 /tmp/shelldir/csq_20240314
-rw-r--r--. 1 root root   0  315 15:18 /tmp/shelldir/csq_20240315
-rw-r--r--. 1 root root   0  315 15:18 /tmp/shelldir/filename_20240313
-rw-r--r--. 1 root root   0  315 15:18 /tmp/shelldir/filename_20240314
-rw-r--r--. 1 root root   0  315 15:18 /tmp/shelldir/filename_20240315
-rwx------. 1 root root 147  315 13:56 /tmp/shelldir/hello-world.sh
-rwxr-xr-x. 1 root root 265  315 14:17 /tmp/shelldir/showname.sh

数值运算:简单的加减乘除

例如:我们要让用户输入两个变量,然后将两个变量的内容相乘,最后输出相乘的接管,那可以怎么做呢?

[root@chenshiren shelldir]# vim cf.sh 
#!/bin/bash
# 说明:
# 用户输入两个数字,程序算出两数乘积输出结果
# 时间: 2024/3
PATH=${PATH}:/tmp/shelldir
export PATH
read -p "输入第一个数:" number1
read -p "输入第二个数:" number2
result=$((${number1}*${number2}))
echo -e "\n乘积结果:${result} \a"

执行结果

[root@chenshiren shelldir]# chmod +x cf.sh ; ./cf.sh
输入第一个数:10
输入第二个数:20000

乘积结果:200000 

在数值运算上面可以使用【declare -i product=${onenumber}*${twonumber}】,也可以使用上面的方式来进行。比较建议使用这种方法

var=$((运算内容))

这种方法很容易记忆。未来你可以使用这种方式来计算。至于数值运算上的处理,则有+、-、*、/、%等。%是取余的,举例来说,15对7取余数,结果就是15=2*7+1,所以余数就是1

[root@localhost shelldir]# echo $((15 % 7))
1

如果你要计算含有小数点的数据时,其实可以通过bc这个命令的协助

[root@localhost shelldir]# echo "123.456*789.123" | bc
97421.969

数值运算:通过bc计算Pi(圆周率)

其实计算Pi,小数点以下位数可以无限地扩展下去,而bc提供了一个运算Pi的函数,要使用该函数必须通过bc -l来调用才行。也因为这个小数点的位数可以无限扩展运算的特性存在,所以我们可以通过下面这个小脚本来让用户输入一个【小数点位】,让Pi能够更准确

[root@chenshiren shelldir]# vim pi.sh 
#!/bin/bash
# 说明:
# 计算圆周率
# 时间:2024/3
PATH=${PATH}:/tmp/shelldir
export PATH
echo  "此程序将计算圆周率"
read -p "请输入计算圆周率后几位小数默认(10):" pi
num=${pi:-"10"}
echo "开始计算,请等待"
time echo "scale=${num};4*a(1)" | bc -lq

[root@chenshiren shelldir]# chmod +x pi.sh ; ./pi.sh
此程序将计算圆周率
请输入计算圆周率后几位小数默认(10):20
开始计算,请等待
3.14159265358979323844

real    0m0.001s
user    0m0.001s
sys     0m0.001s

上述数据中,那个4*a(1) 是bc主动提供的一个计算Pi的函数,至于scale就是要bc计算几个小数点位数的意思。scale的数值越大,代表Pi要被计算得越精确,当然用掉的时间就会越多。

脚本的执行方式差异(source、sh script、./script)

不同的脚本执行方式会造成不一样的结果,尤其对bash的环境影响很大。脚本的执行除了前面使用的【sh script】 以外还可以利用 【source 】或小数点【.】来执行。那么这些执行方式有何不同呢?

利用直接执行的方式来执行脚本

当使用直接命令执行(不论是绝对路径/相对路径还是${PATH}内),或是利用bash(或sh)来执行脚本时,该脚本都会使用一个新的bash环境来执行脚本内的命令。也就是说,使用这种执行方式时,其实脚本是在子进程的bash内执行的。和export差不多,重点在于【当子进程完成后,在子进程内的各项变量或操作会结束而不会传回到父进程中】

我们使用上面缩写的showname.sh脚本做个实验,这个脚本可以让用户设置两个变量,分别是firstname与lastname。想一想,如果你直接执行该行命令,该命令帮你设置的firstname会不会生效呢?

[root@chenshiren shelldir]# echo ${nickname} ${fullname}
            <==两个变量并不存在
[root@chenshiren shelldir]# sh showname.sh 
输入姓名:c
输入全名:csq
你姓:c 
全名叫:csq
[root@localhost shelldir]# echo ${nickname} ${fullname}
       <== 事实上,这两个变量在父进程的bash中还是不存在的

上面的结果很奇怪,怎么我已经利用showname.sh设置号的变量竟然在bash环境下面无效。我们以下图说明

image-20230427205247525

当你使用直接执行的方法来处理时,系统会给予一个新的bash让我们来执行showname.sh 里面的命令,因此你的nickname和fullname等变量其实是在下图的子进程bash内执行的,当showname.sh执行完毕后,子进程bash内的所有数据便被删除,因此【echo ${nickname} ${fullname}】时,就看不到任何东西了。

利用source来执行脚本:在父进程中执行

如果你使用source 来执行命令那就不一样了,同样的脚本我们来执行看看

[root@chenshiren shelldir]# source showname.sh 
输入姓名:c
输入全名:csq
你姓:c 
全名叫:csq 
[root@chenshiren shelldir]# echo ${xm},${qm}
c,csq    # 有数据产生了!

竟然生效了,没错因为source对脚本的执行方式可以使用下面的图例来说明,showname.sh会在父进程中执行,因此各项操作都会在原本的bash内生效。这也是为啥你不注销系统而要让某些写入~/.bashrc的设置生效时,需要使用【source ~/.bashrc】而不能使用【bash ~/.bashrc】。

image-20230427210128792

如何使用shell脚本的判断式

利用test命令的测试功能

当我们要检测上面某些文件或是相关的属性时,利用test这个命令来进行检测

例如我要检查/csq 是否存在时

[root@localhost ~]# test -e /csq
[root@localhost ~]# echo $?
1

【test -e /csq】并没有显示任何信息,当我们输入了 echo $? 返回值不是0 代表是错误信息,我们也可以利用 &&及||来展销整个结果

例如我换一种方法检测一下/csq是否存在

[root@localhost ~]# test -e /csq && echo "this is exist" || echo "this is no exist"
this is no exist

最终结果告诉我们是【exist】还是【no exist】,那我们知道 -e 是测试一个【东西】存在不存在

关于某个文件名的【文件类型】判断

例如:test -e filename 表示存在否

测试的参数代表的意义
-e该【文件名】是否存在
-f该【文件名】是否存在且位文件(file)
-d该【文件名】是否存在且位目录
-b该【文件名】是否存在且为一个block device 设备
-c该【文件名】是否存在且为一个 character device
-S该【文件名】是否存在且为一个socket文件
-p该【文件名】是否存在且为一个FIFO文件
-L该【文件名】是否存在且为一个链接文件

关于文件的权限检测

例如:test -r filename 表示可读否

测试的参数代表的意义
-r检测该文件名是否存在且具有【可读】的权限
-w检测该文件名是否存在且具有【可写】的权限
-x检测该文件名是否存在且具有【可执行】的权限
-u检测该文件名是否存在且具有【SUID】的属性
-g检测该文件名是否存在且具有【SGID】的属性
-k检测该文件名是否存在且具有【SBIT】的属性
-s检测该文件名是否存在且为【非空文件】

两个文件之间的比较

例如:test file1 -nt file2

测试的参数代表的意义
-nt判断【file1】是否比【file2】新
-ot判断【file1】是否比【file2】旧
-ef判断【file1】与【file2】是否为同一个文件,可用在判断硬链接上面。
主要意义在于判定,两个文件是否均指向同一个inode

关于两个整数之间的比较

例如:test n1 -eq n2

测试的参数代表的意义
-eq两数值相等
-ne两数值不相等
-gtn1 大于 n2
-ltn1 小于 n2
-gen1 大于等于 n2
-len1 小于等于 n2

判定字符串的数据

测试的参数代表的意义
test -z string判定字符串是否为0?若string为空字符串,则为true
test -n string判断字符串是否非为0?若string为空字符串,则为false
test str1 == str2判定str1是否等于str2,若相等,则返回true
test str1 != str2判定str1是否不等于str2,若相等,则返回true

多重条件判定

例如:test -r filename -a -x filename

测试的参数代表的意义
-a两条件同时成立。
例如:test -r file -a -x ,则file同时具有r与x权限时,才返回true
-o两条件任何一个成立
例如:test -r file -o -x ,则file具有r或x权限时,旧可返回true
!反相状态
例如:test ! -x file ,当file不具有x时,返回true

例题①

首先我让用户输入一个文件名,我们判断:

  1. 整个文件是否存在,若不存在则给予一个【文件不存在】的信息,并中断程序
  2. 若这个文件存在,则判断它是个文件或目录,结果输出【这个文件是普通文件】或【这个文件是目录文件】
  3. 判断一下,执行者的身份对这个文件或目录所拥有者的权限,并输出权限输出
[root@chenshiren shelldir]# vim panduan.sh
#!/bin/bash
# 说明:
# 用户输入一个文件,程序判断 1.文件是否存在 2. 是目录还是文件 3. 文件权限是什么
# 时间: 2024/3

PATH=${PATH}:/tmp/shelldir
export PATH
read -p "请输入一个文件: " filename
test -z ${filename} && echo "你必须输入一个文件" && exit 0
test ! -e ${filename} && echo "你输入的${filename}不存在无法判断" && exit 0

test -f ${filename} && filetype="普通文件"
test -d ${filename} && filedir="目录文件"
test -r ${filename} && readfile="可读"
test -w ${filename} && writefile="可写"
test -x ${filename} && executefile="可执行"

echo -e "这个文件是: ${filetype}${filedir} \n \
        $LOGNAME用户对这个${filetype}${filedir}拥有的权限有:${readfile},${writefile},${executefile} \r\a"

执行结果(如果文件存在)

[root@chenshiren shelldir]# chmod a+x panduan.sh 
[root@chenshiren shelldir]# sh panduan.sh 
请输入一个文件: /root/pay.txt
这个文件是: 普通文件 
         root用户对这个普通文件拥有的权限有:可读,可写, 

执行结果(如果文件不存在)

[root@chenshiren shelldir]# sh panduan.sh 
请输入一个文件: dd
你输入的dd不存在无法判断

执行结果(在其他用户上面执行)

[csq@chenshiren shelldir]$ sh panduan.sh 
请输入一个文件: /etc
这个文件是: 目录文件 
         csq用户对这个目录文件拥有的权限有:可读,,可执行 

利用判断符号[ ]

其实除了test之外,我们还可以利用【[ ]】判断符号来进行数据判断。举例来说,如果我想知道${HOME}这个变量是否为空,可以这样做:

[root@localhost ~]# [ -z "$HOME" ] ; echo $?
1

使用中括号必须要特别注意,因为中括号用在很多地方,包括通配符和正则表达式等。所以说如果要在bash的语法当中使用中括号作为shell的判断式时,必须要注意中括号的两端需要有空格符来分隔。假设空格键使用【□】符号来表示,那你这些地方都需要有空格

[root@localhost ~]# [□"$HOME"□==□"$MAIL"□]

你会发现上面的判断式中使用了两个等号【==】,其实在bash当中使用一个等号和两个等号的结果是一样的。不过在一般常用程序的写法中,一个等号代表【变量的设置】两个等号则代表【逻辑判断(是与否之意)】由于问在中括号内重点在于【判断】而非【设置变量】,因此建议你使用两个等号

上面的例子在说明,两个字符串【$HOME】与【$MAIL】是否相同,相当于【 test ${HOME} == ${MAIL}】。而如果没有空白分隔例如:[${HOME}==${MAIL}],我们的bash就会显示错误信息。

  • 中括号[]内的每个组件都需要有空格来分隔
  • 在括号内的变量,最好都以双引号括起来
  • 在中括号内的常数,最好都以单或双引号括起来

例题①

  1. 当执行一个程序的时候,这个程序会让用户选择Y或N
  2. 如果用户输入Y或y时,就显示【OK,继续】
  3. 如果用户输入N或n时,就显示【NO!,不继续】
  4. 如果不是Y/yN/n之内的字符,就显示【我不知道你输入的是什么】
[root@localhost shelldir]# vim continue.sh
#!/bin/bash
# 程序说明:
#  这个程序就是让用户做选择
# 时间:
# 2023/04/29
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:~/shelldir
export PATH
read -p "请输入一个Y/y,N/n我们来进行下一步:" yn
[ "${yn}" == "Y" -o "${yn}" == "y" ] && echo "OK,继续" && exit 0
[ "${yn}" == "N" -o "${yn}" == "n" ] && echo "NO,不继续" && exit 0
echo -e "\n我不知道你输入的是什么\a"

由于输入正确(Yes)的方法有大小写之分,不论输入大写或小写y都是可以的,此时判断式内就得要两个判断才行,由于是任何一个成立即可(大写或小写的y),所以这里使用-o连接两个判断。

执行结果如下(输入Y)

[root@localhost shelldir]# sh continue.sh 
请输入一个Y/y,N/n我们来进行下一步:Y
OK,继续

执行结果如下(没输入)

[root@localhost shelldir]# sh continue.sh 
请输入一个Y/y,N/n我们来进行下一步:

我不知道你输入的是什么

shell脚本的默认变量($0、$1…)

我们知道命令可以带有选项参数,例如 【ls -al】可以查看包含隐藏文件的所有的属性与权限。那么shell脚本能不能再脚本文件名后面带有参数呢?

例如:如果你想要重新启动系统网络,可以这样做

[root@chenshiren ~]# file /usr/sbin/NetworkManager    
/usr/sbin/NetworkManager: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=92544dc17cde0154660e122ab223eacc47a0173a, for GNU/Linux 3.2.0, stripped
# 使用file来查询后,系统告知这个文件是个bash可执行的文件
[root@chenshiren ~]# /usr/sbin/NetworkManager restart

restart是重启的意思,上面的命令可以【重新启动 /usr/sbin/NetworkManager这个程序】。那么如果你再 【/usr/sbin/NetworkManager】加个个stop ,就直接关闭服务了。

那么脚本怎么完成这个功能呢?其实脚本针对参数已经设置好了一些变量名称,对应如下

/path/to/scriptname    opt1  opt2  opt3  opt4
     $0                 $1    $2    $3    $4

执行的脚本文件名为 $0这个变量,第一个接的参数就是$1。所以只要我们在脚本里面善用$1的话,就可以很简单地立即执行某些命令功能了。除了这些变量之外,还有一些较为特殊的变量可以在脚本内使用来调用这些参数

  • $#:代表后接的参数【个数】,以上表为例这里显示为【4】
  • $@:代表【"$1" "$2" "$3" "$4"】之意,每个变量都是独立的(用双引号括起来)
  • $*:代表【“$1c$2c$3c$4c”】,其中c为分隔符,默认为空格,所以本例中代表【“$1 $2 $3 $4”】

$@和$*一般情况下可以直接记忆$@

例题①

假设我要执行要个可以携带的参数脚本,执行该脚本后屏幕会显示如下数据:

  • 程序的文件名是什么?
  • 共有几个参数
  • 如果文件名后面没跟参数就提示他没跟参数
  • 若参数的个数小于2则告知用户参数数量太少
  • 全部的参数内容是什么
  • 第一个参数是什么
  • 第二个参数是什么
[root@chenshiren shelldir]# vim args_shibie.sh   
#!/bin/bash
# 程序说明:
#    展示以下执行shell脚本的文件名,以及参数
# 时间:
# 2024/3
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
export PATH
echo "这个文件名是:${0}"
echo "共有$#个参数"
[ "$#" == "0" ] && echo "文件名后面没跟参数"  && exit 0
[ "$#" -lt 2 ]  && echo "只有一个参数 $1 , 参数太少"  && exit 0
echo "全部的参数内容是:$@"
echo "仅展示前3个参数内容"
echo "第一个参数是:$1"
echo "第二个参数是:$2"
echo "第三个参数是: $3"

执行结果(跟了参数)

[root@chenshiren shelldir]# sh args_shibie.sh csq csq1 csq2 csq3 csq4
这个文件名是:args_shibie.sh
共有5个参数
全部的参数内容是:csq csq1 csq2 csq3 csq4
仅展示前3个参数内容
第一个参数是:csq
第二个参数是:csq1
第三个参数是: csq2

执行结果(没跟参数)

[root@chenshiren shelldir]# sh args_shibie.sh 
这个文件名是:args_shibie.sh
共有0个参数
文件名后面没跟参数

shift:造成参数变量号码偏移

除此之外,脚本后面所接的变量是否能够进行偏移(shift)呢?什么是偏移?我们举例来说明,还是用上面的例题。

[root@localhost shelldir]# vim args_shibie.sh
#!/bin/bash
# 程序说明:
#    显示shift造成的参数变量号码偏移了多少
# 时间:
#  2024/3
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
export PATH
echo "共有$#个参数"
echo "全部的参数内容是:$@"
shift
echo "共有$#个参数"
echo "全部的参数内容是:$@"
shift 3
echo "共有$#个参数"
echo "全部的参数内容是:$@"

执行结果如下

[root@localhost shelldir]# sh args_shibie.sh aa bb cc dd ff gg hh
共有7个参数
全部的参数内容是:aa bb cc dd ff gg hh
共有6个参数
全部的参数内容是:bb cc dd ff gg hh
共有3个参数
全部的参数内容是:ff gg hh

看到上面的结果就可以知道,shift会移动变量,而且shift后面可以接数字,代表拿掉最前面的几个参数的意思。上面执行结果中,第一次进行shift后它的显示情况是【 aa bb cc dd ff gg hh 】所以只剩下6个参数,第二次直接拿掉三个,就变成【 aa bb cc dd ff gg hh 】

shell脚本的条件判断式

很多时候我们必须要根据某些数来判断程序该如何举例来说,我们在之前的练习中让用户输入Y/N的时候,必须要执行不同的信息输入,可以使用&& 和||的方式,那么如果我们要执行一堆命令呢

利用 if…then

这个if…then是最常见的条件判断式了。简单来说,当符合某个条件判断的时候,就允许他进行某项任务。这个if…then的判断还有多层次的情况。

单层、简单条件判断式

如果你只有一个判断式要进行。那么我们可以简单地这样看:

if  [ 条件判断式 ]; then
         当条件成立时,可以进行的命令工作内容
fi  # 将if 反过来写,就成为fi,意思就是结束if

如果我们有多个条件判别的话,就像之前的案例所写的,就是将【多个条件写入一个中括号内的情况】,我们还可以有多个括号隔开。而括号和括号之间,则以&&或||隔开

  • &&代表AND
  • ||代表or

所以我们之前使用中括号判别到底输入的是不是Y/y,就可以这样修改

[ "${yn}" == "Y" -o "${yn}" == "y" ]

上面的案例可以替换为

[ "${yn}" == "Y" ] || [ "${yn}" == "y" ]

那么我们再来用if…then的样式来看看:

image-20230430150507499

上图是我们之前案例的脚本内容我们来做一下修改

[root@chenshiren shelldir]# vim yesno.sh 
#!/bin/bash
# 说明: 让用户输入Y或N,程序判断输入的是Y还是N,再做出输出
# 时间: 2024/3
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:~/shelldir
export PATH
read -p "请输入一个Y/y,N/n我们来进行下一步:" yn
if [ "${yn}" == "Y" -o "${yn}" == "y" ];then
   echo "OK,继续"
   exit 0
fi

if [ "${yn}" == "N" -o "${yn}" == "n" ];then
   echo "NO,不继续"
   exit 0
fi
echo -e "\n我不知道你输入的是什么\a"

执行结果(输入Y看结果)

[root@chenshiren shelldir]# sh yesno.sh  
请输入一个Y/y,N/n我们来进行下一步:y
OK,继续

执行结果(什么也没输入)

[root@chenshiren shelldir]# sh yesno.sh 
请输入一个Y/y,N/n我们来进行下一步:

我不知道你输入的是什么

多重、复杂条件判断式

就像上述例子一样,如果该数据需要进行多种不同的判断,我只想执行一次${yn}的判断就好,不想做多次if的判断,那样应该怎么做呢?

一个条件判断,分成功执行与失败执行(else)

if [ 条件判断式 ]; then
      当条件判断式成立时,可执行的命令
else
      当条件判断式不成立时,可执行的命令
fi

如果你遇到非常复杂的判断情况,则可以使用这个语法

if [ 条件判断式 ]; then
      当条件判断式成立时,可执行的命令
elif [ 条件判断式2 ]; then
       当条件判断式2成立时,可执行的命令
else 
       当条件判断式1与2均不成立时,可执行的命令
fi

要注意的是,【elif】也是个判断式,因此【elif】后面都要接then来处理。else则是最后没有成立的结果,不用加then。我们可以将之前的案例改为

[root@chenshiren shelldir]# vim yesno.sh 
#!/bin/bash
# 说明: 让用户输入Y或N,程序判断输入的是Y还是N,再做出输出
# 时间: 2024/3
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:~/shelldir
export PATH
read -p "请输入一个Y/y,N/n我们来进行下一步:" yn
if [ "${yn}" == "Y" -o "${yn}" == "y" ];then
   echo "OK,继续"
   exit 0
elif [ "${yn}" == "N" -o "${yn}" == "n" ];then
   echo "NO,不继续"
   exit 0

else
    echo -e "\n我不知道你输入的是什么\a"
fi

程序是不是变得非常容易看懂了,可以避免重复判断的状况。

例题1

一般来说,如果你不希望用户键盘输入额外的数据,则可以使用【$1】参数功能,让用户执行命令时将参数带进去。现在我想让用户输入【hello】这个关键字时,利用参数的方法可以这样依序设计

  1. 判断$1是否为hello,如果是的话,就显示"你好,最近过的好吗?"
  2. 如果没有加任何参数,就提示用户必须要使用的参数执行法;
  3. 而如果加入的参数不是hello,就提醒用户仅能使用hello为参数。
[root@chenshiren shelldir]# vim howareyou.sh
#!/bin/bash
# 说明: 判断允许该脚本是否加了hello参数
# 时间:2024/3

PATH=${PATH}:/tmp/shelldir
export PATH

if [ "${1}" == "hello" -o "${1}" == "HELLO" ];then
        echo -e "你好,最近过的好吗?"
        exit 0
elif [ -z "${1}" ];then
        echo -e "\n必须使用的参数执行法\a"
        exit 0
else
        echo -e "\n仅能识别hello为参数\a"
fi

执行结果(输入hello)

[root@chenshiren shelldir]# sh howareyou.sh hello
你好,最近过的好吗?

执行结果(什么也没输入)

[root@chenshiren shelldir]# sh howareyou.sh

必须使用的参数执行法

执行结果(随便添加了一个参数)

[root@chenshiren shelldir]# sh howareyou.sh ppp

仅能识别hello为参数

接下来再来了解一个命令利用这个命令来做几个实验,学一个叫netstat的命令,这个命令可以查询到目前主机开启的网络服务端口,利用【netstat -tuln】来获取目前主机启动的服务

如果没有netstat这条命令可以使用

yum install -y net-tools来安装相关的命令

[root@localhost ~]# netstat -tuln 
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN     
tcp6       0      0 :::22                   :::*                    LISTEN     
tcp6       0      0 ::1:25                  :::*                    LISTEN     
udp        0      0 127.0.0.1:323           0.0.0.0:*                          
udp6       0      0 ::1:323                 :::*    

输出的内容中,最重要的就是 【Local Address(本地主机的IP与端口对应)】那个字段,它代表的是本机所启动的网络服务,IP的部分说明的是该服务器位于哪个接口上。若为【127.0.0.1】则是针对本机开发,若是【0.0.0.0】或【:::】则代表对整个internet开放。每个端口都有其特点的网络服务,几个常见的端口与相关网络服务的关系是:

  • 80:WWW
  • 22:ssh
  • 21:ftp
  • 25:mail
  • 111:RPC(远程过程调用)
  • 631:CPUS(打印机服务功能)

假设我的主机要检测比较常见的21、22、25及80端口时,那我如何通过netstat去检测我的主机是否开启了这四个主要的网络服务端口?由于每个服务的关键词都是接在冒号【:】后面,所以可以使用类似【:80】的方式来检测

[root@chenshiren shelldir]# vim netstat.sh
#!/bin/bash
# 说明: 检测21 22 25 80 端口是否存在
# 时间: 2024/3
PATH=${PATH}:/tmp/shelldir
export PATH
testfile=/tmp/shelldir/testfile.txt
netstat -tulnp > ${testfile}

testing=$(grep ":80" ${testfile})
if [ "${testing}" != "" ] ;then
        echo "存在80端口"
else
        echo "不存在80端口"
fi
testing=$(grep ":25" ${testfile})
if [ "${testing}" != "" ] ;then
        echo "存在25端口"
else
        echo "不存在25端口"
fi
testing=$(grep ":22" ${testfile})
if [ "${testing}" != "" ] ;then
                echo "存在22端口"
else
        echo "不存在22端口"
fi
testing=$(grep ":21" ${testfile})
if [ "${testing}" != "" ] ;then
        echo "存在21端口"
else
        echo "不存在21端口"
fi
[root@chenshiren shelldir]# sh netstat.sh 
不存在80端口
不存在25端口
存在22端口
不存在21端口

例题2

条件判断式还可以搞的更复杂。举例来说,每当过年的时候都能收到压岁钱,写个脚本,每当我输入过年的时间的时候,都会提示我还有几个月过年。

  1. 先让用户输入过年的时间
  2. 用现在的时间对比过年时间
  3. 计算出离过年还剩下多少天

可以利用【date --date=“YYYYMMDD”+%s】转成秒数后,就很容易操作了

[root@chenshiren shelldir]# vim guonian.sh
#!/bin/bash
# 说明:
# 用户输入一个时间计算还需要多久过年
# 时间: 2024/3

PATH=${PATH}:/tmp/shelldir
export PATH
declare -i date_xianzai1=$(date +%Y%m%d)
read -p "请输入准确的过年时间:" time
date_pd=$( echo ${time} |grep -E '^[0-9]{8}$')
if [ "$date_pd" == "" ];then
        echo  -e "\n请输入正确时间\a"
        exit 1
elif [ "$date_pd" -lt "$date_xianzai1" ];then
        echo -e "\n请输入正确时间\a"
        exit 1
fi


declare -i date_shuru=$(date --date="${time}" +%s)
declare -i date_xianzai=$(date +%s)
declare -i date_js=$(( ${date_shuru}-${date_xianzai} ))
declare -i date_js_hs=$(( ${date_js} /60/60/24))
declare -i date_js_hours=$(( ${date_js} /60/60%24 ))
echo -e  "离过年还有${date_js_hs}${date_js_hours}小时"
[root@chenshiren shelldir]# sh guonian.sh 
请输入准确的过年时间:20250129
离过年还有318天5小时

上面的程序可以计算离过年还剩下多少天。其中的【${date_hsstdin}】变量中的 /60/60/24 就是将秒数换算成天数。

%24:这个符号是取模运算符,表示取除以24(一天的小时数)的余数

利用case…esac判断

上面提到的【if … then … fi】对于变量的判断是以【比对】的方式来分辨的,如果符合状态就进行某些操作,并且通过较多层次(就是 elif …)的方式来进行多个变量的程序代码编写。【case…esac】语句用于对变量或参数多重比较,如果匹配成功,就执行一段语句,否则执行其他语句

它的语法格式如下

case  $变量名称 in         # 关键字为case,还有变量前面有美元符号
  "第一个变量内容")         # 每个变量内容建议用双引号括起来,关键字则为右圆括号
         程序段            
           ;;             # 每个类别结尾使用两个连续的分号来处理
  "第二个变量内容")
         程序段
           ;;
  *)                      # 最后一个变量内容都会用*来代表所有其他值
           exit 1
           ;; 
esac                      # 最终的case结尾,是【case】反过来写。

来修改以下上述的例题1,他应该会变成这样

#!/bin/bash
PATH=${PATH}:/tmp/shelldir
export PATH

case ${1} in
        "hello")
                echo "hello,how are you?"
                ;;
        "")
                echo "你没有加hello参数"
                ;;
        *)
                echo "你参数输入错了"
                ;;
esac

执行结果

[root@chenshiren shelldir]# sh howareyou.sh 
你没有加hello参数
[root@chenshiren shelldir]# sh howareyou.sh hello
hello,how are you?
[root@chenshiren shelldir]# sh howareyou.sh dadaadc
你参数输入错了

例题1

让用户输入one、two、three并且将用户的变量显示到屏幕上;如果输入的不是one、two、three就告诉用户只有这三种选择

这个是直接执行式

[root@chenshiren shelldir]# vim ots.sh
#!/bin/bash
# 说明: 直接执行程序,程序后面只能跟one、two、three三个参数
# 时间: 2024/3
PATH=${PATH}:/tmp/shelldir
export PATH

case ${1} in
        "one")
                echo "这是参数one"
                ;;
        "two")
                echo "这是参数two"
                ;;
        "three")
                echo "这是参数three"
                ;;

esac
[root@chenshiren shelldir]# sh ots.sh  one
这是参数one
[root@chenshiren shelldir]# sh ots.sh   two 
这是参数two
[root@chenshiren shelldir]# sh ots.sh  three
这是参数three

这个是交互式

[root@chenshiren shelldir]# vim ots.sh
read -p "输入参数: " arg
#!/bin/bash
# 说明: 用户输入一个参数,程序判断有没有这个参数
# 时间: 2024/3
PATH=${PATH}:/tmp/shelldir
export PATH

read -p "输入参数: " arg
case ${arg} in
        "one")
                echo "这是参数one"
                ;;
        "two")
                echo "这是参数two"
                ;;
        "three")
                echo "这是参数three"
                ;;
        *)
                echo  -e "请输入正确的参数\a"
                ;;

esac
[root@chenshiren shelldir]# sh ots.sh 
输入参数: 0
请输入正确的参数
[root@chenshiren shelldir]# sh ots.sh 
输入参数: one
这是参数one

利用function功能

什么是【函数(function)】功能?简单来说,其实,函数可以在shell脚本当中做出一个类似自定义执行命令的东西,最大的功能就是可以简化我们很多的程序代码。举例来说,上面的案例中,每个输入结果one、two、three其实输出的内容都一样,那么我就可以使用function来简化

function的语法是这样的

function fname () {
       程序段
}

那个fname就是我们自定义的执行命令名称,而程序段就是我们要它执行的内容了。要注意的是,shell脚本的执行方式是由上到下,从左至右,因此在shell脚本当中的function的设置一定要在程序的最前面,这样才能够在执行时被找到可用的程序段

例题1

我们将上述的case…esac的例题改一下,自定义一个名为printit的函数来使用:

[root@chenshiren shelldir]# vim  function.sh 
#!/bin/bash
# 程序说明:
#    使用function来选择参数
# 时间:2024/3
PATH=${PATH}:/tmp/shelldir
export PATH

function print () {
        echo -n "你选择的参数为:"
}

case ${1} in
        "one")
                print; echo ${1} | tr 'a-z' 'A-Z'
                ;;
        "two")
                print; echo ${1} | tr 'a-z' 'A-Z'
                ;;
        "three")
                print; echo ${1} | tr 'a-z' 'A-Z'
                ;;
        *)
                echo -e "\n只能选择{one|two|three}参数\a"
                ;;
esac

上面的例子中,声明了一个函数 print ,所以,当我们在后续的程序段中,只要执行print的话,就表示我的shell脚本要去执行【function print】里面的那几个程序段。

另外,function也拥有内置变量的,它的内置变量与shell脚本很类似,函数名称$0,后续接的变量也是以$1、$2..来替换,【function fname () {程序段}】内的$0,$1…等与shell脚本的$0是不同的。

以上面的例题1来改变一下进行说明,假如我执行【sh function.sh one】,表示shell脚本内的$1为"one"这个字符串,但是在print( )内的$1则与这个one无关。

[root@chenshiren shelldir]# vim  function.sh 
#!/bin/bash
# 程序说明:
#    使用function来选择参数
# 时间:2024/3
PATH=${PATH}:/tmp/shelldir
export PATH

function print () {
        echo -n "你选择的参数为: ${1}"
}

case ${1} in
        "one")
                print 1
                ;;
        "two")
                print 2
                ;;
        "three")
                print 3
                ;;
        *)
                echo -e "\n只能选择{one|two|three}参数\a"
                ;;
esac
[root@chenshiren shelldir]# sh function.sh one
你选择的参数为: 1[root@chenshiren shelldir]# 

在上面的例子中,如果你输入【sh function.sh one】就会出现【你选择的参数为:1】如下图

image-20230504105203965

因为我们在程序段落中,我们写了【printit 1】,那个1就会成为function当中的$1。

$()和$(())讲解

$()是命令替换的语法,将括号内的命令执行后的结果作为一个整体返回。例如:

echo $(ls)

$(())是算数扩展语法,用于执行算术运算。例如:

echo $((2+3))

$(( ))内可以使用的算术运算符包括 +、-、*、/、%、<<、>>、&、|、^ 等。而$()内可以执行任何命令,包括算术运算。
因此,$()用于执行命令替换,$(( ))用于执行算术运算。两者的语法和用途不同。

shell脚本的循环

除了if…then…fi 这种条件判断式之外,循环可能是程序当中最重要的一环了。循环可以不断地执行某个程序段落,直到用户设置的条件完成为止。所以,重点是那个【条件的完成】是什么,除了这种依据判断式完成与否的不定循环之外,还有另外一种已经固定要跑多少次循环状态,可称为固定循环状态。

while do done、until do done(不定循环)

一般来说,不定循环最常见的就是下面的两种状态了:

while  [ condition ]  # 中括号内的状态就是判断式 
do       # do是循环的开始
         程序段     
done     # done 是循环的结束

while的中文是【当…时】,所以,这种方式说的是【当condition条件成立时,就进行循环,直到condition的条件不成立才停止】的意思,还有另外一种不定循环的方式

until [ condition ]
do
       程序段
done

这种方式恰恰与while相反,它说的是【当condition条件成立时,就终止循环,否则就持续进行循环的程序段】

例题1

假设我要让用户输入yes或是YES才结束程序的执行,否则就一直告诉用户输入字符串

#!/bin/bash
# 程序说明:
#   输入yes/YES停止输出字符串
# 时间:
# 2024/03
PATH=${PATH}:/tmp/shelldir
export PATH
while [ "${yn}" != "yes" -a "${yn}" != "YES"  ]
do
      read -p "请输入yes/YES来停止该程序:" yn
done
      echo -e "\n您已经停止了该程序\a"

上面这个例题当中【当 ${yn} 这个变量不是 “yes” 且 ${yn} 也不是 "YES"时,就进行循环的程序】,如果输入了yes 或YES 就退出循环。

例题2

我们改变一下上述案例使用 until do done 的形式循环程序

[root@chenshiren shelldir]# vim yes.sh
#!/bin/bash
# 说明: 当用户输入yes或YES程序停止循环
# 时间: 2024/3
PATH=${PATH}:/tmp/shelldir
export PATH

until [ "${yn}" == "yes" -o "${yn}" == "YES" ]       
do
        read -p "请输入yes/YES来停止该程序:" yn
done
        echo -e "\n您已经停止了该程序\a"

上面这个例题当中【当 ${yn} 这个变量是"YES" 或是 “yes” 就退出循环】否则就持续进行循环。

例题3

如果我要计算1+2+…+100的结果呢?利用循环

[root@chenshiren shelldir]# vim 100.sh
#!/bin/bash
# 说明: 求1+...100的和
# 时间: 2024/3
PATH=${PATH}:/tmp/shelldir
export PATH
s=0
i=0
while [ "${i}" != "100" ]
do
        i=$(( $i+1 ))
        s=$(( $s+$i ))
done 
        echo "1+2+...100=${s}"

当你的执行结果为5050这个数据时就对了。

例题4

如果让用户自行输入一个数字,让程序1+2+3…加到你输入的数字为止,该如何编写呢?

[root@chenshiren shelldir]# vim 100.sh
#!/bin/bash
# 说明: 输入一个数字该程序将从1+到你输入的这个数字为止
# 时间: 2024/3
PATH=${PATH}:/tmp/shelldir
export PATH
read -p "请输入一个数字:" number
s=0
i=0
while [ "${i}" != "${number}" ]
do
        i=$(( $i+1 ))
        s=$(( $s+$i ))
done
        echo "1+2+...加到你输入的这个数字和为${s}"
[root@chenshiren shelldir]# sh 100.sh
请输入一个数字:20000
1+2+...加到你输入的这个数字和为200010000

是不是很简单,也可以使用until do done 来测试一下

for…do…done(固定循环)

相对于while、until的循环方式是必须要【符合某个条件】的状态,for这种语法,则是【已经知道要进行几次循环】的状态

它的语法为

for  var in con1 con2 con3 .....
do
     程序段
done

以上面的例子来说,这个${var}的变量内容在循环工作时:

  1. 第一次循环时,${var} 的内容为 con1
  2. 第二次循环时,${var} 的内容为 con2
  3. 第三次循环时,${var} 的内容为 con3

例题1

假设我有3种动物,分别是dog、cat、sheep(🐏),我想每一行都输出这样:【There are dogs…】之类的字样

#!/bin/bash
# 程序说明:
# 该程序判断动物园有哪些动物
# 时间:
# 2024/3
PATH=${PATH}:/tmp/shelldir
export PATH
echo -e "今天的动物园有哪些动物呢?"
for animal in dog cat tiger
do
      echo "有${animal}s"
done

例题2

如果我想要找到/etc/passwd 内的第一个字段,能不能通过管道命令的cut识别出单纯的账号名称后,以id分别检查用户的标识符与特殊参数(id 用户名)?

#!/bin/bash
# 程序说明:
# 该程序识别passwd的账户名称后,用id 账户名称查看用户标识符与特殊参数
# 时间:
# 2024/3
PATH=${PATH}:/tmp/shelldir
export PATH
cutpasswd=$(cut -d ':' -f1 /etc/passwd )   # 选取账号名称
for username in ${cutpasswd}               # 开始循环
do
   id ${username}
done

执行结果如下

[root@localhost shelldir]# sh userid.sh 
uid=0(root) gid=0(root)=0(root)
uid=1(bin) gid=1(bin)=1(bin)
uid=2(daemon) gid=2(daemon)=2(daemon)
uid=3(adm) gid=4(adm)=4(adm)
uid=4(lp) gid=7(lp)=7(lp)
uid=5(sync) gid=0(root)=0(root)
uid=6(shutdown) gid=0(root)=0(root)
uid=7(halt) gid=0(root)=0(root)
uid=8(mail) gid=12(mail)=12(mail)
uid=11(operator) gid=0(root)=0(root)
uid=12(games) gid=100(users)=100(users)
.....
...

例题3

假如我利用ping这个可以判断网络状态的命令,来进行网络状态的实际检测时,我想要检测的域名是本机所在的192.168.124.1~192.168.124.100网段,1~100 ,总不会我在for后面输入 1到100吗?

[root@chenshiren shelldir]# vim ip_ping.sh
#!/bin/bash
# 说明: 检测网段
# 时间: 2024/3
PATH=${PATH}:/tmp/shelldir
export PATH

network=192.168.200
for jcwd in $(seq 1 100)
do      
        ip=${network}.${jcwd}
        ping -c 1 -w 1 ${ip} > /dev/null 2>&1
        if [ "$?" -eq "0" ];then
                echo "${ip}通"
        else    
                echo "${ip}不通"
        fi      
done                                     # 和done

# seq 1 100是一个用于生成从1到100的整数序列的命令

例题4

我想要让用户输入某个目录文件名,然后我找出某目录内的文件名权限,应该怎么做呢?

[root@chenshiren shelldir]# vim cat_rwx.sh
do
#!/bin/bash
# 说明: 查看目录下的文件权限
# 时间: 2024/3
PATH=${PATH}:/tmp/shelldir
export PATH
read -p "输入目录或文件名:" dir
if [ "${dir}" == "" -o ! -d "${dir}" ];then
        echo "该${dir}目录不存在"
        exit 1
fi
filelist=$(ls ${dir})
for filename in ${filelist}
do      
        test -r "${dir}/${filename}" && perm="可读"
        test -w "${dir}/${filename}" && perm="${perm},可写"
        test -x "${dir}/${filename}" && perm="${perm},可执行"
        echo "这个文件${dir}/${filename}本机用户拥有的权限是${perm}"
done

for…do…done的数值处理

除了上述方法之外,for循环还有另外一种写法,语法如下:

for  (( 初始值; 限制值; 赋值运算 ))
do  
       程序段
done

这种语法适合于数值方面的运算当中,for后面括号内的三串内容意义是:

  • 初始值:某个变量在循环当中的起始值,直接类似 i=1 设置好;
  • 限制值:当变量的值在这个限制值的范围内,就继续进行循环,例如 i<=100
  • 赋值运算:每做一次循环,变量也变化,例如:i=i+1

值得注意的是,在【赋值运算】的设置上,如果每次增加1,则可以使用类似【i++】的方式,就是i每次循环都会增加1的意思。

例题

从1累加到用户输入的数值

[root@chenshiren shelldir]# vim user_number.sh 
#!/bin/bash
# 说明: 从1累加到用户输入的数值
# 时间: 2024/3
PATH=${PATH}:/tmp/shelldir
export PATH

read -p "请输入一个数值:" number

s=0
for (( i=1; i<=${number}; i=i+1 ))
do
        s=$(($s+$i))
done
        echo "1+2....${number}=${s}"

搭配随机数与数组的实验

说到随机数肯定会用到系统给我提供的这个变量${RANDOM}

${RANDOM} 是一个 Bash 内置的环境变量,用于生成一个随机整数。每次调用 ${RANDOM} 时,都会生成一个 0 到 32767之间的随机整数。
可以使用以下方式来获取 ${RANDOM} 的值:

echo ${RANDOM}

也可以将 ${RANDOM} 的值赋值给变量:

my_random=${RANDOM}
echo ${my_random}

由于 ${RANDOM} 只是一个环境变量,所以它的值只在当前 Shell 进程中有效。如果需要在脚本中生成多个随机数,可以在需要的地方调用 ${RANDOM}。

例题1

假如你在家,你不知道吃什么饭,选择困难就很烦,那你就可以写个脚本,脚本搭配随机数来告诉你,今天中午吃啥好?执行这个脚本后,直接跟你说要吃什么。

应该怎么做呢?首先你得要将全部的店家输入到一组数组当中,再通过随机数的处理,去获取可能的数值,再将搭配到的数值显示出来即可。

#!/bin/bash
# 程序说明:
#   打印今天中午吃什么饭
# 时间:
# 2024/3
PATH=${PATH}:/tmp/shelldir
export PATH
eat[1]="红烧肉"           # 定义一个数组
eat[2]="糖醋排骨"
eat[3]="小炒牛肉"
eat[4]="小炒五花肉"
eat[5]="平菇炒香干"
eat[6]="香菇炒芹菜"
eat[7]="喝西北风"
eat[8]="奥里给"
eat[9]="泡面"
eatnum=9                # 定义变量9,表示午餐可选的菜品
check=$(( ${RANDOM} * ${eatnum} /32767 +1))   # 通过随机数计算出今天中午吃什么菜
echo "你中午吃${eat[${check}]}"

上面案例中最重要的就是随机数了,【${RANDOM} * ${eatnum} /32767 +1 】计算出 check 变量的值,${RANDOM} 表示 Bash 内置的环境变量,用于生成一个随机整数,每次调用 ${RANDOM} 时,都会生成一个 0 到 32767 之间的随机整数,这里将其乘以菜品数量,再除以 32767,最后加 1,得到一个 1 到 9 之间的随机整数。

当我们执行上述案例时,就知道自己要吃啥了非常的方便。

例题2

那么如果我想吃3个菜呢?而且不能重复一样的,那应该怎么做?

#!/bin/bash
PATH=${PATH}:/tmp/shelldir
export PATH
eat[1]="红烧肉"
eat[2]="糖醋排骨"
eat[3]="小炒牛肉"
eat[4]="小炒五花肉"
eat[5]="平菇炒香干"
eat[6]="香菇炒芹菜"
eat[7]="喝西北风"
eat[8]="奥里给"
eat[9]="泡面"
eatnum=9
eated=0
while [ "${eated}" -lt "3" ]; do 
    check=$(( ${RANDOM} * ${eatnum} /32767+1 ))
    mycheck=0
    if [ "${eated}" -ge "1"  ]; then
        for i in $(seq 1 ${eated})
        do
               if [ ${eatedcon[$i]} == $check  ]; then
                      mycheck=1
               fi
        done
    fi
    if [ "${mycheck}" == "0"  ]; then
       echo "你可以吃${eat[${check}]}"
       eated=$((${eated} + 1 ))
       eatedcon[${eated}]=${check}
    fi
done

代码解释

这段脚本用于随机选择三种食物,输出供用户选择。脚本中的变量和数组含义如下:

  • PATH:环境变量,指定可执行文件的搜索路径。

  • eat:数组,包含九种食物。

  • eatnum:变量,表示数组元素个数。

  • eated:变量,表示已经选择的食物数量,初始值为0。

  • check:变量,用于存储随机选择的食物在数组中的索引。

  • mycheck:变量,用于判断已经选择的食物中是否已经包含了当前选中的食物。

  • eatedcon:数组,用于存储已经选择的食物在数组中的索引。

while循环中的逻辑如下:

  • 当已经选择的食物数量小于3时,进行循环。

  • 生成一个随机数check,表示在数组中的索引。

  • 判断当前选中的食物是否已经被选择过,如果是则跳过,否则输出当前选中的食物,并将eated加1,同时将选中的食物在数组中的索引存入eatedcon数组中。

执行结果

[root@k8s-master-node1 shelldir]# sh noon_what_eat.sh 
你可以吃奥里给
你可以吃平菇炒香干
你可以吃糖醋排骨

shell脚本的跟踪与调试

脚本文件在执行之前,最怕就是出现语法错误的问题。那么我们如何调试呢?有没有办法不需要通过直接执行脚本文件就可以判断是否有问题?我们直接用bash的相关参数

sh [-nvx] scripts.sh
选项与参数:
-n:不要执行脚本,仅查询语法的问题
-v:再执行脚本前,先将脚本文件的内容输出到屏幕上
-x:将使用到的脚本内容显示到屏幕上

使用案例

测试dir_perm.sh 有无语法问题

[root@k8s-master-node1 shelldir]# sh -n dir_rwx.sh
# 若没有语法问题,则不会显示任何信息

将noon_what_eat.sh 的执行过程全部列出来

[root@k8s-master-node1 shelldir]# sh -x noon_what_eat.sh 
+ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/bin:/root/shelldir
+ export PATH
+ eat[1]=红烧肉
+ eat[2]=糖醋排骨
+ eat[3]=小炒牛肉
+ eat[4]=小炒五花肉
+ eat[5]=平菇炒香干
+ eat[6]=香菇炒芹菜
+ eat[7]=喝西北风
+ eat[8]=奥里给
+ eat[9]=泡面
+ eatnum=9
+ eated=0
+ '[' 0 -lt 3 ']'
+ check=2
.....
.......
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值