7: Shell脚本基础

1. 编程基础

1.1 程序组成

程序: 算法+数据结构
数据: 是程序的核心
数据结构: 数据在计算机中的类型和组织方式
算法: 处理数据的方式

1.2 程序编程风格

面向过程语言:

  • 做一件事情, 排出个步骤, 第一步干什么, 第二步干什么, 如果出现情况A, 做什么处理, 如果出现了情况B, 做什么处理
  • 问题规模小, 可以步骤化, 按步就班处理
  • 以指令为中心, 数据服务于指令
  • C, Shell

面向对象语言:

  • 一种认识世界, 分析世界的方法论. 将万事万物抽象为各种对象
  • 类是抽象的概念, 是万事万物的抽象, 是一类事物的共同特征的集合
  • 对象是类的具象, 是一个实体
  • 问题规模大, 复杂系统
  • 以数据为中心, 指令服务于数据
  • java, C#, Python, Golang等

1.3 编程语言

计算机:运行二进制指令

编程语言:人与计算机之间交互的语言。分为两种:低级语言和高级语言

低级编程语言:

机器:二进制的0和1的序列,称为机器指令。与自然语言差异太大,难懂、难写
汇编:用一些助记符号替代机器指令,称为汇编语言
如:ADD A,B 将寄存器A的数与寄存器B的数相加得到的数放到寄存器A中
汇编语言写好的程序需要汇编程序转换成机器指令
汇编语言稍微好理解,即机器指令对应的助记符,助记符更接近自然语言

高级编程语言:

编译:高级语言-->编译器-->机器代码文件-->执行,如:C,C++
解释:高级语言-->执行-->解释器-->机器代码,如:shell,python,php,JavaScript,perl

编译和解释型语言:

图片.png

编译型语言不支持跨平台, 因为编译型语言的运行需要先把源代码通过编译器转换成二进制文件, 最终运行的是二进制文件. 而不同平台的编译器将源代码编译后的二进制文件格式不同, 因此编译型语言不支持跨平台

解释型语言如Python, 支持跨平台, 因为解释型语言是靠解释器运行源代码. 如果需要跨平台, 那么只需要在不同平台安装对应版本解释器, 就可以直接运行源代码

  • Java
图片.png

Java支持跨平台, 将源代码转换成字节码后, 把字节码移植到不同的操作系统, 通过不同操作系统上的Java虚拟机去转换成二进制文件

1.4 编辑逻辑处理方式

顺序结构流程:

图片.png

分支结构流程:

图片.png

循环结构流程:

图片.png

三种处理逻辑:

  • 顺序执行:程序按从上到下顺序执行
  • 选择执行:程序执行过程中,根据条件的不同,选择不同分支继续执行
  • 循环执行:程序执行过程中需要重复执行多次某段语句

2. Shell脚本语言的基本用法

2.1 Shell脚本的用途

自动化常用命令
执行系统管理和故障排除
创建简单的应用程序
处理文本或文件

2.2 Shell脚本基本结构

Shell脚本编程:是基于过程式、解释执行的语言

编程语言的基本结构:

  • 各种系统命令的组合
  • 数据存储:变量、数组
  • 表达式:a + b
  • 控制语句:if

Shell脚本:包含一些命令或声明,并符合一定格式的文本文件

格式要求:首行shebang机制

#!/bin/bash
#!/usr/bin/python
#!/usr/bin/perl 

2.3 Shell脚本创建过程

第一步:使用文本编辑器来创建文本文件

  • 第一行必须包括Shell声明序列:#!
示例:
#! /bin/bash
  • 添加注释,注释以#开头

第二步:加执行权限

  • 给予执行权限,在命令行上指定脚本的绝对或相对路径

第三步:运行脚本

  • 直接运行解释器,将脚本作为解释器程序的参数运行

2.4 Shell 脚本注释规范

1、第一行一般为调用使用的语言
2、程序名: 避免更改文件名后无法找到正确的文件
3、版本号
4、更改后的时间
5、作者相关信息
6、该程序的作用,及注意事项
7、最后是各版本的更新简要说明

2.5 第一个Shell脚本

hello.sh

#!/bin/bash
echo "Hello World"  

执行脚本方式

通过bash命令可以直接执行脚本, 无需给脚本赋予执行权限

[02:46:25 root@CentOS7 ~/scripts]#bash hello.sh
Hello World

[02:46:39 root@CentOS7 ~/scripts]#cat hello.sh | bash
Hello World

[02:46:50 root@CentOS7 ~/scripts]#bash < hello.sh 
Hello World

可以使用wget或者curl命令从网站上把脚本下载到服务器, 然后传给bash执行

将脚本本身内容作为标准输入, 传给bash

cat test.sh | bash
bash < test.sh
curl http://www.xxx.com/test.sh | bash
[root@demo-c8 ~]# curl -s http://www.xxx.com/testdir/hello.sh | bash 
hello, world
Hello, world!

不通过bash命令执行, 那么就需要给脚本添加执行权限:

1. 将脚本文件所在路径加入到PATH变量, 即可直接执行, 直接写脚本文件名称, 无需指定路径
2. 将脚本放在PATH变量路径里, 也可直接执行, 直接写脚本文件名称, 无需指定路径
3. 如果没有把脚本放到PATH里, 就要指定脚本的绝对路径或者相对路径

案例: 备份/etc整个目录到/tmp目录下

#!/bin/bash
  
GREEN="echo -e \033[1;31m"
END="\033[0m"
${GREEN}Starting to back up...${END}
sleep 5
cp -a /etc /tmp/etc_`date +%F_%T`  #/tmp/etc_`date +%F_%T`, 目标目录或者文件会自动按照时间创建
[ `echo $?` = 0 ] && echo "Backup Complete" || echo "Backup Failed"

2.6 Shell脚本调试

  • 语法错误: 会导致后续的命令不继续执行, 可以用bash -n检查错误, 不过提示的出错行不一定是准确的
  • 命令错误: 后续的命令还会继续执行, 用bash -n无法检查出来, 可以用bash -x进行观察, 因此, 执行脚本前一定要确保脚本无误, 尤其是执行数据备份时. 因为, 一旦出错可能会丢失数据
  • 逻辑错误: 只能使用bash -x进行观察, 显示每一步详细的执行过程, 有助于排错, 但是bash -x会真正执行脚本

2.7 变量

2.7.1 变量

变量表示命名的内存空间, 将数据放在内存空间中, 通过变量名引用, 获取数据

传统开发语言, 需要先声明变量类型, 再赋值, 使用变量

Shell无需对变量声明类型, 默认对所有变量都当成字符串, 直接赋值变量, 即可使用变量

2.7.2 变量类型

变量类型:

  • Shell内置变量,如:PS1,PATH,UID,HOSTNAME,$$,BASHPID,PPID,$?,HISTSIZE
  • 用户自定义变量: 用户在脚本中根据需求自定义

不同的变量存放的数据不同, 取决于以下因素:

  • 数据存储方式
  • 参与的运算
  • 表示的数据范围

变量数据类型:

  • 字符
  • 数值: 整型, 浮点型, 注意, bash不支持浮点数

2.7.3 编程语言分类

静态和动态语言

  • 静态编译语言:使用变量前,先声明变量类型,之后类型不能改变,在编译时检查,如:Java,C, Go
  • 动态编译语言:不用事先声明,可随时改变类型,如:bash,Python

强类型和弱类型语言

  • 强类型语言:不同数据类型间运算操作,必须经过强制转换为同一类型才能运算,如Java , C# ,Python

如:参考以下 Python 代码

 print('aaa'+ 10) 提示出错,不会自动转换类型
 print('aaa'+str(10)) 结果为aaa10,需要显示转换类型
  • 弱类型语言:语言的运行会隐式做数据类型转换。无须指定类型,默认均为字符型;参与运算会自动进行隐式类型转换;变量无须事先定义可直接调用
    如:bash ,php,Javascript

2.7.4 Shell 中变量命名规则

  • 不能使用程序中的保留字:如:if, for
  • 只能使用数字、字母及下划线,且不能以数字开头,注意:变量名不支持短横线 “ - ”,和主机名相反, 主机名不支持下划线
  • 见名知义,用英文单词命名,并体现出实际作用,不要用简写,如:ATM
  • 统一命名规则:驼峰命名法, studentname,大驼峰StudentName, 小驼峰studentName
  • 环境变量名字大写:STUDENT_NAME
  • 局部变量小写
  • 函数名小写

2.7.5 变量的定义和饮用

按照变量的生效范围可划分为:

环境变量: 生效范围为当前Shell进程及其子进程,如$PATH, $PS1等系统环境变量, 也可以自定义环境变量

普通变量: 生效范围为当前Shell进程;对当前Shell之外的其它Shell进程,包括当前Shell的子Shell都不生效. 一般用在Shell终端或者脚本中定义. 
在脚本中定义的普通变量是无法在Shell终端调用的

本地变量: 也叫局部变量, 生效范围为当前Shell进程中某代码片断,通常指函数, 一般定义在函数内

变量赋值

Shell的变量赋值要求变量名, 等号和变量值之间不能有空格

name=value

value可是以下多种形式

1. 字符串:name='root'

2. 变量引用:name="$USER"

[03:23:31 root@CentOS7 ~]#name="$SHELL"
[03:25:06 root@CentOS7 ~]#echo $name
/bin/bash

3. 命令引用:name=`COMMAND` 或者 name=$(COMMAND)

[03:25:09 root@CentOS7 ~]#name=`pwd`
[03:25:36 root@CentOS7 ~]#echo $name
/root

注意:变量赋值是临时生效的,退出终端后,变量会自动删除,无法持久保存,脚本中的变量会随着脚本结束,也会自动删除

变量引用

$name
${name}: 通过花括号, 限定变量的边界

弱引用和强引用

"$name" 弱引用,其中的变量引用会被替换为变量值
'$name' 强引用,其中的变量引用不会被替换为变量值,而保持原字符串
  • 范例:变量的各种赋值方式和引用
[03:25:41 root@CentOS7 ~]#TITLE='CEO'
[03:29:58 root@CentOS7 ~]#echo $TITLE
CEO
[03:30:02 root@CentOS7 ~]#echo i am $TITLE
i am CEO
[03:30:11 root@CentOS7 ~]#echo 'i am $TITLE' # 单引号不会扩展变量
i am $TITLE
[03:30:31 root@CentOS7 ~]#echo "i am $TITLE" # 双引号会扩展变量
i am CEO
[03:31:21 root@CentOS7 ~]#echo `echo $TITLE` # 反引号既扩展变量也扩展命令
CEO
[03:30:38 root@CentOS7 ~]#echo `i am $TITLE` # $TITLE被反引号扩展为CEO, 反引号内部的字符串会被当作命令执行, 只是i am CEO不是命令, 所以才会报错
-bash: i: command not found
[03:31:53 root@CentOS7 ~]#NAME=$USER
[03:33:56 root@CentOS7 ~]#echo $NAME
root
[03:34:01 root@CentOS7 ~]#USER=`whoami`
[03:34:09 root@CentOS7 ~]#echo $USER
root
[03:34:12 root@CentOS7 ~]#FILE=`ls /run`
[03:34:46 root@CentOS7 ~]#echo $FILE
auditd.pid console crond.pid cron.reboot cryptsetup dbus faillock initramfs lock log mount netreport NetworkManager plymouth sepermit setrans sshd.pid sudo syslogd.pid systemd tmpfiles.d tuned udev user utmp vmware
[03:34:48 root@CentOS7 ~]#FILE=/etc/* # 用空格隔开的连续字符串, 会被依次赋值给变量, 并且也是以空格隔开
[03:35:21 root@CentOS7 ~]#echo $FILE
/etc/adjtime /etc/aliases /etc/aliases.db ...
[03:37:13 root@CentOS7 ~]#seq 10
1
2
3
4
5
6
7
8
9
10
[03:37:14 root@CentOS7 ~]#NUM=`seq 10`
[03:37:19 root@CentOS7 ~]#echo $NUM
1 2 3 4 5 6 7 8 9 10 # 列赋值会被转为行赋值
[03:37:35 root@CentOS7 ~]#NAME="david
> wang
> hahaha"
[03:38:45 root@CentOS7 ~]#echo $NAME
david wang hahaha

[03:39:00 root@CentOS7 ~]#echo "$NAME" # 如果想要列赋值仍然返回列赋值, 那么就用双引号把变量扩起来
david
wang
hahaha

显示多个变量:

[23:06:56 root@c8prac ~]#type=it
[23:07:56 root@c8prac ~]#book=linux
[23:08:08 root@c8prac ~]#echo $type:$book
it:linux

不能用下划线连接, 因为Shell定义变量是可以用下划线的, 如果两个变量中间用下划线连接,那么Shell会认为type_是个变量, 而不是type是一个变量

[23:08:21 root@c8prac ~]#echo $type_$book
linux

解决方法: 利用花括号, 指定变量名的边界. 建议脚本中的变量尽量用花括号, 避免产生问题

[23:10:48 root@c8prac ~]#echo ${type}_$book
it_linux

可以用横线连接, 因为Shell定义变量是不能用横线的, 因此Shell认为type是一个变量, book是一个变量, 而横线就是一个连接符

[23:09:30 root@c8prac ~]#echo $type-$book
it-linux

变量赋值案例:

将一个变量A的结果赋值给另一个变量B后, 此时如果修改变量A的值, 那么变量B的值不变.

[23:12:48 root@c8prac ~]#name=david
[23:16:27 root@c8prac ~]#title=$name
[23:16:52 root@c8prac ~]#echo $title
david
[23:16:57 root@c8prac ~]#name=linux
[23:17:04 root@c8prac ~]#echo $title
david

原因:

一个变量名和值做了捆绑之后, 变量名就指向了该值所在的内存空间. 除非针对这个变量名重新做赋值或者把原有值删除, 否则不会变化

image.png

单引号, 双引号, 反引号的区别

echo 'echo $name'  #单引号, 所见即所得, 不会扩展命令和变量
echo $name

echo "echo $name" #双引号, 只扩展变量, 不会扩展命令. 识别$name为linux后, echo linux并不会被当做命令执行, 而是当做字符串, 因此结果是echo linux
echo linux

echo `echo $name` #反引号, 即扩展命令又扩展变量, 先把$name识别为linux, 再执行echo linux命令得到linux, 最后执行echo linux得到linux. 
linux 

动态命令

将命令赋值给变量, 执行, 在脚本中常用

把一个命令, 放到一个变量里, 通过$变量名, 达到执行效果

由于命令是存在变量里的, 而命令是可以修改的, 因此达到了动态命令的效果

动态命令中, 如果要引用变量, 那么要用双引号

[23:25:58 root@c8prac ~]#CMD='hostname'
[23:36:52 root@c8prac ~]#$CMD
c8prac.linux

动态命令 vs 别名

别名在交互式环境使用, 而动态命令是在脚本中使用

别名在脚本中是不生效的

  • 多行字符赋值给变量, 并且显示为多行
[23:40:56 root@c8prac ~]#COURSE="it
> math
> music
> sport
> food"
[23:48:06 root@c8prac ~]#echo $COURSE
it math music sport food
[23:48:12 root@c8prac ~]#echo "$COURSE"
it
math
music
sport
food

变量追加, 在变量原有基础上, 追加字符串

[23:48:17 root@c8prac ~]#NAME=Michael
[23:51:05 root@c8prac ~]#echo $NAME
Michael
[23:51:09 root@c8prac ~]#NAME+=:Jordan
[23:51:17 root@c8prac ~]#echo $NAME
Michael:Jordan
[root@demo-c8 ~]# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
[root@demo-c8 ~]# PATH+=:/opt
[root@demo-c8 ~]# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:/opt

等价于重新赋值

[23:51:20 root@c8prac ~]#NAME=Michael
[23:52:50 root@c8prac ~]#echo $NAME
Michael
[23:52:54 root@c8prac ~]#NAME=$NAME:Jordan
[23:53:06 root@c8prac ~]#echo $NAME
Michael:Jordan

set命令: 显示自定义变量, 系统内置变量, 和函数, 服务器所有的变量都会显示

env命令: 只显示环境变量

unset命令: 删除变量. 变量不使用时, 最好把变量删除. 但是一般在脚本里, 变量都是普通变量, 只在当前进程(脚本)里有效, 脚本执行结束了, 变量也就删除了.

Shell脚本案例: 打印服务器基本信息

#!/bin/bash
RED="\E[1;31m"
GREEN="echo -e \E[1;32m"
END="\E[0m"
.  /etc/os-release

$GREEN----------------------Host systeminfo--------------------$END
echo -e  "HOSTNAME:     $RED`hostname`$END"
#echo -e  "IPADDR:       $RED` ifconfig eth0|grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' |head -n1`$END"
echo -e  "IPADDR:       $RED` hostname -I`$END"
echo -e  "OSVERSION:    $RED$PRETTY_NAME$END"
echo -e  "KERNEL:       $RED`uname -r`$END"
echo -e  "CPU:         $RED`lscpu|grep '^Model name'|tr -s ' '|cut -d : -f2`$END"
echo -e  "MEMORY:       $RED`free -h|grep Mem|tr -s ' ' : |cut -d : -f2`$END"
echo -e  "DISK:         $RED`lsblk |grep '^sd' |tr -s ' ' |cut -d " " -f4`$END"
$GREEN---------------------------------------------------------$END
[root@demo-c8 ~]# bash sysinfo.sh 
----------------------Host systeminfo--------------------
HOSTNAME:     demo-c8.demo
IPADDR:       10.0.0.108 192.168.122.1 
OSVERSION:    CentOS Linux 8 (Core)
KERNEL:       4.18.0-193.el8.x86_64
CPU:          Intel(R) Core(TM) i7-10875H CPU @ 2.30GHz
MEMORY:       7.6Gi
DISK:         100G
---------------------------------------------------------

练习:

  1. 编写脚本systeminfo.sh, 显示当前主机系统信息, 包括: 主机名, ipv4地址, 操作系统版本, 内核版本, cpu型号, 内存大小, 硬盘大小
  1. 编写脚本backup.sh, 可实现每日将/etc目录备份到目录/backup/etcYYYY-mm-dd

  2. 编写脚本disk.sh, 显示当前硬盘分区中空间利用率最大的值

  3. 编写脚本links.sh, 显示正连接本机的每个远程主机的ipv4地址和连接数, 并按连接数从大到小排序

2.7.6 环境变量

  • 父进程和子进程

Linux的进程可以进程嵌套, 在进程1中开启的其他进程都属于进程1的子进程

范例: 在f1.sh脚本中, 调用f2.sh

# f1.sh
[root@demo-c8 data]# vim f1.sh

#!/bin/bash

echo f1.sh
./f2.sh

# f2.sh

[root@demo-c8 data]# vim f2.sh

#!/bin/bash
echo f2.sh
sleep 100

[root@demo-c8 data]# ll
total 8
-rw-r--r--. 1 root root 35 Sep  8 12:06 f1.sh
-rw-r--r--. 1 root root 33 Sep  8 12:05 f2.sh
[root@demo-c8 data]# chmod +x *
[root@demo-c8 data]# ll
total 8
-rwxr-xr-x. 1 root root 35 Sep  8 12:06 f1.sh
-rwxr-xr-x. 1 root root 33 Sep  8 12:05 f2.sh
[root@demo-c8 data]#./f1.sh 
f1.sh
f2.sh

           ├─sshd(1150)─┬─sshd(3479)───sshd(3484)───bash(3492)───pstree(3622)
           │            └─sshd(3564)───sshd(3568)───bash(3581)───f1.sh(3619)───f2.sh(3620)───sleep(3621)
  • 普通变量只在当前Shell有效, 环境变量在当前Shell以及其子Shell,子子Shell都有效.

范例: 普通变量只在当前Shell有效

# f1.sh

[root@demo-c8 data]# vim f1.sh 
#!/bin/bash
name="admin"
echo f1.sh
echo f1.sh:$name
./f2.sh

# f2.sh

[root@demo-c8 data]# vim f2.sh 

#!/bin/bash
echo f2.sh
echo f2.sh:$name  # $name是在f2.sh的父进程, f1.sh中定义的, 因此, 无法传递给子进程
sleep 100      
[root@demo-c8 data]# ./f1.sh 
f1.sh
f1.sh:admin
f2.sh
f2.sh:

如果想让父进程定义的变量, 传递给子进程, 那么需要把变量定义为环境变量

父进程定义的环境变量可以传给子进程和子子进程, 但是子进程定义的环境变量无法传给父进程

范例: 将name普通变量定义为环境变量, 传递给子进程

# f1.sh

[root@demo-c8 data]# vim f1.sh 

#!/bin/bash
export name="admin"
echo f1.sh
echo f1.sh:$name
./f2.sh


# f2.sh

[root@demo-c8 data]# vim f2.sh 

#!/bin/bash
echo f2.sh
echo f2.sh:$name
sleep 100
[root@demo-c8 data]# ./f1.sh 
f1.sh
f1.sh:admin
f2.sh
f2.sh:admin

一旦在子进程修改了从父进程继承的环境变量, 那么将会把新的值传递给孙子进程

范例: 在子进程f2.sh中, 修改name变量的值, 验证会把新的值传给孙子进程f3.sh, 同时父进程不受影响, 但是f2.sh本身会受影响

# f1.sh

[root@demo-c8 data]# vim f1.sh 

#!/bin/bash
export name="admin"  # 父进程中, 将name定义为环境变量, 值为admin
echo f1.sh
echo f1.sh:$name
./f2.sh

# f2.sh

[root@demo-c8 data]# vim f2.sh 

#!/bin/bash
echo f2.sh
echo f2.sh:$name # 子进程可以继承父进程中的环境变量
name="root"  # 子进程也可修改从父进程继承的环境变量
echo f2.sh:$name # 修改后, 子进程会受到影响
./f3.sh

# f3.sh

[root@demo-c8 data]# vim f3.sh 

#!/bin/bash
echo f3.sh
echo $name  # 孙子进程也会从父进程继承环境变量, 但是如果子进程修改了环境变量, 那么孙子进程的环境变量会收到影响
sleep 100
[root@demo-c8 data]# ./f1.sh 
f1.sh
f1.sh:admin
f2.sh
f2.sh:admin
f2.sh:root
f3.sh
root

一般只在系统配置文件中使用环境变量, 因为操作系统运行时会产生很多子进程, 但只有一个祖宗进程, 所以需要环境变量来传递值, 但是在脚本中较少使用, 因为脚本很少会嵌套其他脚本

变量声明和赋值: 临时声明环境变量

#自定义环境变量, 并且声明和赋值
export name=VALUE
declare -x name=VALUE
#或者分两步实现
name=VALUE
export= name

环境变量在脚本中很少定义, 因为脚本中很少会嵌套其他脚本

一般定义在系统中, 比如PATH变量, 这些变量在所有Shell进程中都会用到

环境变量一旦在文件中声明, 后续直接使用即可, 无需再次声明

范例: 永久声明环境变量

# 将自定义的环境变量写到bash的配置文件里, 之后source或者重新登录终端

全局配置: 对于所有登录到终端的用户都有效

/etc/profile
/etc/profile.d/*.sh
/etc/bashrc

个人配置: 只对当前登录终端的用户有效

~/.bash_profile
~/.bashrc

显示当前终端进程编号

echo $$
或者
echo $BASHPID

显示父进程ID

echo $PPID

常用环境变量

_ #下划线表示前一个命令的最后一个参数, 如果命令没有参数, 那就是命令本身

[23:55:49 root@c8prac ~]#ls /data
prac  scripts  testing_scripts
[23:56:04 root@c8prac ~]#echo $_
/data

显示所有环境变量

env
printenv
export
declare -x

删除变量

unset 变量名

bash内建的环境变量

PATH
SHELL
USER
UID
HOME
PWD
SHLVL # Shell的嵌套层数, 即深度
LANG
MAIL
HOSTNAME
HOSTSIZE
_ # 下划线表示前一个命令的最后一个参数

2.7.7 只读变量

只读变量: 只能声明定义, 但后续不能修改和删除, 即常量

声明只读变量: 当前Shell临时有效

readonly name
declare -r name

查看只读变量:

readonly [-p]  #不加参数就是显示所有只读变量, 比如UID, BASHPID等. 
UID是只读变量, 其代表的是登录到终端的用户身份, 如果UID可以随意被修改, 那么用户身份也就可以被随意修改了
declare -r

范例:

readonly X=3 #只读变量就是把一个变量定义为常亮, 之后无法修改也无法删除. 想删除就要退出当前Shell

[22:47:49 root@centos8 ~]#readonly PI=3
[23:36:35 root@centos8 ~]#readonly PI=4
-bash: PI: readonly variable
[23:36:40 root@centos8 ~]#PI=5
-bash: PI: readonly variable
[22:47:49 root@centos8 ~]#readonly PI=3
[23:36:35 root@centos8 ~]#readonly PI=4
-bash: PI: readonly variable
[23:36:40 root@centos8 ~]#PI=5
-bash: PI: readonly variable
[23:37:00 root@centos8 ~]#echo $PI
3
[23:37:16 root@centos8 ~]#logout
Connection to 192.168.192.8 closed.
david@davids-MacBook-Pro ~ % ssh [email protected]
[email protected]'s password: 
Last login: Wed Sep  7 22:47:49 2022 from 192.168.192.1
[23:37:35 root@centos8 ~]#echo $PI

[23:37:39 root@centos8 ~]#

2.7.8 位置变量

位置变量: 在bash shell中内置的变量, 可以在脚本代码中调用命令行传递给脚本的参数

$1, $2,... 对应第1个, 第2个等参数, shift [n] 换位置
$0, 脚本文件本身, 包括脚本路径, 如果不想包括路径, `basename $0`
$* 传递给脚本的所有参数, 并且把全部参数何为一个字符串
$@ 传递给脚本的所有参数, 但是把每个参数为独立字符串
$# 传递给脚本的参数的个数
注意: $@ $*只有在被双引号括起来的时候才会有差异

范例: 位置参数范例

[root@demo-c8 ~]# vim /data/test.sh 

#!/bin/bash
echo $0
basename $0
echo $1
echo $2
echo $3
echo $4
echo $*
echo $@
echo $#
[root@demo-c8 ~]# /data/test.sh  1 2 3 4 5
/data/test.sh
test.sh
1
2
3
4
1 2 3 4 5
1 2 3 4 5
5

[root@demo-c8 ~]# bash /data/test.sh 1 2 3 4 5
/data/test.sh
test.sh
1
2
3
4
1 2 3 4 5
1 2 3 4 5
5

[root@demo-c8 ~]# cd /data
[root@demo-c8 data]# ./test.sh 1 2 3 4 5
./test.sh
test.sh
1
2
3
4
1 2 3 4 5
1 2 3 4 5
5
  • @区别演示

$*: 把脚本的参数当做一个整体, 一个字符串

[00:26:39 root@c8prac /data/scripts]#vim test_var1.sh

#!/bin/bash
echo "$*"
test_var2.sh "$*"

[00:26:14 root@c8prac /data/scripts]#vim test_var2.sh

#!/bin/bash
echo $1

[00:28:38 root@c8prac /data/scripts]#bash -x test_var1.sh a b c
+ echo 'a b c'
a b c
+ test_var2.sh 'a b c'
a b c

$@: 把脚本的每一个参数, 当成单独的一个字符串

[00:28:43 root@c8prac /data/scripts]#vim test_var1.sh

#!/bin/bash
echo "$@"
test_var2.sh "$@"

[00:30:44 root@c8prac /data/scripts]#vim test_var2.sh

#!/bin/bash
echo $1

[00:30:56 root@c8prac /data/scripts]#bash -x test_var1.sh a b c
+ echo a b c
a b c
+ test_var2.sh a b c
a

注意: $@ 和 $*只有在被双引号""括起来的时候才会有差异

  • 脚本位置参数举例:

实现安全删除命令: 将删除的文件, 移动到/tmp文件夹

[00:30:59 root@c8prac /data/scripts]#mkdir dir1
[00:35:39 root@c8prac /data/scripts]#touch dir1/test1.txt
[00:35:47 root@c8prac /data/scripts]#mkdir dir2
[00:35:51 root@c8prac /data/scripts]#touch dir2/test2.txt
[00:35:55 root@c8prac /data/scripts]#mkdir dir3
[00:35:58 root@c8prac /data/scripts]#touch dir3/test3.txt

[00:38:54 root@c8prac /data/scripts]#vim rm.sh 
#!/bin/bash
  
RED='echo -e \033[1;31m'
END='\033[0m'
DIR=/tmp/`date +%F_%T`

mkdir $DIR
mv $* $DIR &> /dev/null && ${RED}$* have been moved to $DIR${END} || ${RED}Please make sure yor really need to delete them! ${END}


[00:37:41 root@c8prac /data/scripts]#alias rm=rm.sh  
# 通过给脚本定义别名为rm, 这样rm -rf强行删除命令就无法使用, 可避免误操作, 不过rm -f还是可以执行的, 只不过把-f当成了mv命令第一个参数, 而mv命令是不支持-r选项的, 所以rm -rf无法执行

# 因此, 修改了别名后, 还是要禁用rm -rf, rm -f命令, 防止误删除, 并且不能使用rm原始命令, 否则还是会执行删除命令, \rm

[00:39:41 root@c8prac /data/scripts]#rm dir2
dir2 have been moved to /tmp/2020-10-23_00:39:45
[00:39:45 root@c8prac /data/scripts]#rm dir3
dir3 have been moved to /tmp/2020-10-23_00:39:48
  • 位置参数是无法人为修改的
[01:03:11 root@c8prac /data/scripts]#vim var.sh

#!/bin/bash
echo $1
$1=linux
echo $1
[01:06:03 root@c8prac /data/scripts]#var.sh a b c
a
/data/scripts/var.sh: line 3: a=linux: command not found  #修改位置参数变量后, 会被展开当做命令, 无法执行
a

2.7.9 退出状态码

进程/命令执行后,将使用变量$?保存状态码的相关数字,不同的值反应前一个命令执行成功或失败,$? 取值范围 0-255

$?的值为0  #代表成功
$?的值是1到255   #代表失败

主要用于条件判断, 配合流程控制

[root@demo-c8 ~]# grep -q root /etc/passwd 
[root@demo-c8 ~]# echo $?
0
[root@demo-c8 ~]# id admin
id: ‘admin’: no such user
[root@demo-c8 ~]# id admin &> /dev/null && user exists || useradd admin
[root@demo-c8 ~]# id admin
uid=1001(admin) gid=1001(admin) groups=1001(admin)

范例: 检测网站可用性, 如果失败, 那么重启服务

# 可以配合定时任务, 每一分钟, 执行一次, 失败的话就连接到服务器进行重启
curl -s http://www.xxx.com
if [$? -ne 0]; then
   ssh [email protected] "nginx -s restart"

$?显示最后一条命令执行结果, 对于脚本也一样, 如果想在脚本执行结束退出后, 用$?判断脚本执行结果, 可以用$?, 但是$?只能显示脚本中最后一条命令的执行结果

如果一个命令的执行结果, 既有成功也有失败的, 那么就会返回失败

[root@demo-c8 ~]# ls /boot /xxx
ls: cannot access '/xxx': No such file or directory
/boot:
config-4.18.0-193.el8.x86_64  initramfs-0-rescue-eaab49a3b5f746ae847c443ec9bc62c4.img  loader                            vmlinuz-0-rescue-eaab49a3b5f746ae847c443ec9bc62c4
efi                           initramfs-4.18.0-193.el8.x86_64.img                      lost+found                        vmlinuz-4.18.0-193.el8.x86_64
grub2                         initramfs-4.18.0-193.el8.x86_64kdump.img                 System.map-4.18.0-193.el8.x86_64
[root@demo-c8 ~]# echo $?
2
[root@demo-c8 ~]# ls /xxx /boot
ls: cannot access '/xxx': No such file or directory
/boot:
config-4.18.0-193.el8.x86_64  initramfs-0-rescue-eaab49a3b5f746ae847c443ec9bc62c4.img  loader                            vmlinuz-0-rescue-eaab49a3b5f746ae847c443ec9bc62c4
efi                           initramfs-4.18.0-193.el8.x86_64.img                      lost+found                        vmlinuz-4.18.0-193.el8.x86_64
grub2                         initramfs-4.18.0-193.el8.x86_64kdump.img                 System.map-4.18.0-193.el8.x86_64
[root@demo-c8 ~]# echo $?
2

自定义退出状态码: 系统默认的状态码为0-255, 0表示前一个命令执行成功, 非0表示执行失败

exit NUM: 在脚本中定义退出状态码, 0也可以定制
脚本中, 一旦遇到exit命令, 脚本会立即终止, 终止退出状态码取决于exit命令后面的数字
如果整个脚本都没指定退出状态码, 那么整个脚本的退出状态码取决于脚本中执行的最后一条命令的状态码
即使是执行失败的命令, 也可以把状态码定义为成功的0

范例: 默认的退出状态码

[root@demo-c8 data]# vim test.sh

#!/bin/bash
echo start
cmd
echo finish
[root@demo-c8 data]# bash test.sh 
start
test.sh: line 3: cmd: command not found
finish
[root@demo-c8 data]# echo $?
0 # 脚本的最后一条命令时echo finish, 执行成功, 所以返回0

范例: 将失败的命令的状态码定义为0

[root@demo-c8 data]# vim test.sh

#!/bin/bash
echo start # echo start正常执行
cmd # cmd命令不存在, 返回错误
exit 0 # exit 0, 脚本直接退出, 返回状态码为0
echo finish # 脚本退出, echo finish不执行
[root@demo-c8 data]# bash test.sh 
start
test.sh: line 3: cmd: command not found
[root@demo-c8 data]# echo $?
0

举例:

grep -q root /etc/passwd #grep -q选项无论有没有匹配结果, 都不会显示
echo $? #可以配合$?查看是否匹配成功
0

2.7.10 展开命令行

Shell命令行命令展开顺序:

把命令行分成单个命令词
展开别名
展开大括号的声明{}
展开波浪符声明 ~
命令替换$() 和 ``
再次把命令行分成命令词
展开文件通配*、?、[abc]等等
准备I/0重导向 <、>
运行命令

转义符号: , 反斜线会使随后的字符按照愿意解释

范例:

ls --help | grep '\-s'
[root@demo-c8 data]# echo your cost: $500.00
your cost: 00.00
[root@demo-c8 data]# echo your cost: \$500.00
your cost: $500.00

!: 历史命令替换

  115  id admin
  116  cd /data
  117  ls
  118  vim test.sh
  119  bash test.sh 
  120  echo $?
  121  vim test.sh
  122  bash test.sh 
  123  echo $?
  124  vim test.sh
  125  echo $?
  126  bash test.sh 
  127  echo $?
  128  echo your cost: $500.00
  129  echo your cost: \$500.00
  130  cd
  131  history
[root@demo-c8 ~]# !115
id admin
uid=1001(admin) gid=1001(admin) groups=1001(admin)

2.7.11 脚本安全和set

set命令: 可以用来定制Shell环境

$-变量:

h:hashall,打开选项后,Shell 会将执行的命令hash下来,避免每次都要查询。通过set +h将h选项关闭
i:interactive-comments,包含这个选项说明当前的 shell 是一个交互式的 shell。所谓的交互式shell, 在脚本中,i选项是关闭的
m:monitor,打开监控模式,就可以通过Job control来控制进程的停止、继续,后台或者前台执行等
B:braceexpand,大括号扩展, {}, 开始时, {}会扩展, 关闭时, {}不会扩展
H:history,H选项打开,可以展开历史列表中的命令,可以通过!感叹号来完成,例如“!!”返回上最近的一个历史命令,“!n”返回第 n 个历史命令. 关闭H选项后, 仍然可以执行history命令查看命令历史, 但是无法通过!调用历史命令, 会提示command not found

范例: $-变量

[root@demo-c8 ~]# echo $-
himBHs

变量安全隐患:

一旦某个变量$VAR被不小心删除了, 但是后续还在命令或者脚本中引用, 就会返回空
这时如果执行了rm -rf $VAR/*等类似有安全隐患的命令, 就会有问题

set命令实现脚本安全:

[root@demo-c8 ~]# help set
-u 在扩展一个没有设置的变量时, 显示错误信息, 等同set -o nounset. 而且后续命令不会执行, 脚本直接退出.
-e 如果一个命令返回一个非0退出状态值(失败)就退出, 等同set -o errexit. 这样后续命令不再执行, 默认情况, 当一个命令返回非0状态, 后续命令还会执行
-o option 显示, 打开或者关闭某个选项
    显示选项: set -o
    打开选项: set -o 选项
    关闭选项: set +o 选项
-x 当执行命令时, 打印命令及其参数, 类似bash -x

范例: 在脚本的第一行, 添加set -ue实现脚本安全

set -ue

2.8 格式化输出 printf

内部命令

格式: 通过指定的格式模板, 将后续的文本转换成指定的格式

printf "指定的格式" "文本1" "文本2" ... "文本n"

常用格式替换符:

替换符       功能
%s           字符串
%f           浮点格式
%b           相对应的参数中包含转义字符时,可以使用此替换符进行替换,对应的转义字符会被转义
%c           ASCII字符,即显示对应参数的第一个字符
%d,%i        十进制整数
%o           八进制值
%u           不带正负号的十进制值
%x           十六进制值(a-f)
%X           十六进制值(A-F)
%%           表示%本身

说明:

%s 中的数字代表此替换符中的输出字符宽度,不足补空格,默认是右对齐,%-10s表示10个字符宽,- 表示左对齐
printf默认输出信息不换行; echo默认换行
如果想让echo不换行, 需要使用-n选项, echo -n
如果想让printf换行, 需要使用\n转义字符, printf "\n"

常用转义字符:

转义符         功能
\a             警告字符,通常为ASCII的BEL字符
\b             后退
\f             换页
\n             换行
\r             回车
\t             水平制表符
\v             垂直制表符
\              表示\本身

范例: %s

# 将1 2 5 4 8换行显示
# %s代表一个文本
[11:59:01 root@c8prac ~]#printf "%s\n" 1 2 5 4 8 
1
2
5
4
8
# 每两个文本放在一行显示
# 每个%s代表一个文本
[root@demo-c8 ~]# printf "%s %s\n" {a..h}
a b
c d
e f
g h
[root@demo-c8 ~]# printf "%s %s\n" {a..i}
a b
c d
e f
g h
i 

范例: %f

[root@demo-c8 ~]# printf "%f\n" 1 2 3
1.000000
2.000000
3.000000
[root@demo-c8 ~]# printf "%f %f\n" 1 2 3
1.000000 2.000000
3.000000 0.000000
# .2f表示保留2位小数

[root@demo-c8 ~]# printf "%.2f\n" 1
1.00
[12:21:48 root@c8prac ~]#printf '(%s)' 1 2 4 5  478 97  #printf默认不换行
(1)(2)(4)(5)(478)(97)[12:22:42 root@c8prac ~]#printf '(%s)' 1 2 4 5  478 97;echo
(1)(2)(4)(5)(478)(97)
[root@demo-c8 ~]# printf "(%s)" 1 2 3 4 5;echo " "
(1)(2)(3)(4)(5) 
[root@demo-c8 ~]# printf " (%s) " 1 2 3 4 5;echo ""
 (1)  (2)  (3)  (4)  (5) 
[root@demo-c8 ~]# printf " (%s) " 1 2 3 4 5;echo 
 (1)  (2)  (3)  (4)  (5) 
[12:27:00 root@c8prac ~]#VAR="Hello World!"; printf "\033[1;32m%s\033[0m\n" $VAR
Hello
World!
[12:34:14 root@c8prac ~]#VAR="Hello World!"; printf "\033[1;32m%s\033[0m\n" "$VAR"  #如果加了双引号, 就会把前面定义的变量当做一个整体, 因此即使加了\n也不会换行了
Hello World!
# %-10s, 表示左对齐, 默认printf是右对齐

[root@demo-c8 ~]# printf "%10s\n" 1
         1
[root@demo-c8 ~]# printf "%-10s\n" 1
1         
[root@demo-c8 ~]# printf "%-10s %-10s %-4s   %s \n" 姓名 性别 年龄 体重 小明 男 20 70 小红 女 18 50
姓名     性别     年龄   体重 
小明     男        20     70 
小红     女        18     50 

范例: 进制转换

# 将十进制的17转换为16进制

[root@demo-c8 ~]# printf "%X\n" 17  # %X表示16进制, [A-F]
11
# 将十六进制的C转换成十进制

[root@demo-c8 ~]# printf "%d \n" 0xC  # 16进制以0x开始
12 

2.9 算术运算

Shell允许在某些情况下对算术表达式进行求值,比如:let和declare 内置命令,(( ))复合命令和算术扩展。求值以固定宽度的整数进行,不检查溢出,尽管除以0会被标记为错误。

运算符及其优先级,关联性和值与C语言相同。

以下运算符列表分组为等优先级运算符级别。级别按降序排列优先。

注意: bash算数运算只支持整数, 不支持小数

乘法符号在有些场景中需要进行转义

id++ id-- variable post-increment and post-decrement
++id --id variable pre-increment and pre-decrement
- +   unary minus and plus
! ~   logical and bitwise negation
**     exponentiation 乘方
* / % multiplication, division, remainder, %表示取模,即取余数,示例:9%4=1,5%3=2 + -   addition, subtraction
<< >> left and right bitwise shifts
<= >= < >       comparison
== != equality and inequality
&     bitwise AND
^     bitwise exclusive OR
|     bitwise OR
&&     logical AND
||     logical OR
expr?expr:expr       conditional operator
= *= /= %= += -= <<= >>= &= ^= |=         assignment
expr1 , expr2     comma

实现算数运算:

(1) let var=算术表达式
(2) ((var=算术表达式)) 和上面等价
(3) var=$[算术表达式] 
(4) var=$((算术表达式))
(5) var=$(expr arg1 arg2 arg3 ...)
(6) declare -i var = 数值
(7) echo '算术表达式' | bc

举例: RANDOM随机数内置变量

RANDOM为内置随机数变量, 每次返回一个随机数字, (0-32767)

生成1-50随机数

echo $((RANDOM%50+1))
echo $[RANDOM%50+1]
[22:25:34 root@centos-7-1 ~]#((var=RANDOM%50+1))
[22:25:59 root@centos-7-1 ~]#echo $var
15

随机颜色

# 如果需要把变量引用的结果, 嵌入到命令中, 那么可以用$((算术表达式))或者$[算数表达式]
# 如: echo $[8&4]; echo $((8&4)); 如果用let那么就要写两个命令let var=8\&4; echo $var更繁琐
echo -e "\033[1;$[RANDOM%7+31]mHello World\033[0m"  # 内容不能有!等运算符号, 会有冲突

引用变量何时需要加$何时不用?

echo -e "\033[1;$[RANDOM%7+31]mHello World\033[0m"
echo -e "\033[1;$[$RANDOM%7+31]mHello World\033[0m"

如果命令能识别一个字符串是变量, 那么就不用加$. 比如在算术表达式里, 都是数字, 但是变量是字符, 因此Shell会把字符串认为是变量

注意: 通过算术运算结果定义变量时, 一定要用算数运算符, 否则会被认为是字符串

[13:22:52 root@c8prac ~]#x=10+20
[13:22:59 root@c8prac ~]#echo $x
10+20
[13:23:01 root@c8prac ~]#x=[10+20]
[13:23:08 root@c8prac ~]#echo $x
[10+20]
[13:23:11 root@c8prac ~]#x=$[10+20]
[13:23:21 root@c8prac ~]#echo $x
30
[13:23:23 root@c8prac ~]#let y=10+20
[13:23:41 root@c8prac ~]#echo $y
30
[13:23:43 root@c8prac ~]#((x=1+2))
[13:24:06 root@c8prac ~]#echo $x
3

expr命令: 专门用来做算数运算. 但是每个参数之间要➕空格, 而且算数符号需要转义, 否则有可能会被当做通配符

[13:24:08 root@c8prac ~]#expr 1+2
1+2
[13:25:41 root@c8prac ~]#expr 1 + 2
3
[13:25:45 root@c8prac ~]#expr 1 * 2
expr: syntax error: unexpected argument ‘anaconda-ks.cfg’
[13:25:47 root@c8prac ~]#expr 1 \* 2
2

增强型赋值:

+= i+=10 相当于 i=i+10
-= i-=j   相当于 i=i-j
*=
/=
%=
++ i++,++i   相当于 i=i+1
-- i--,--i   相当于 i=i-1

范例:

[root@demo-c8 ~]# let i=10*2
[root@demo-c8 ~]# echo $i
20
[root@demo-c8 ~]# let i=10**2
[root@demo-c8 ~]# echo $i
100
[root@demo-c8 ~]# ((j=1+2))
[root@demo-c8 ~]# echo $j
3
[root@demo-c8 ~]# i=10
[root@demo-c8 ~]# let i+=20
[root@demo-c8 ~]# echo $i
30
[root@demo-c8 ~]# j=20
[root@demo-c8 ~]# let i*=j
[root@demo-c8 ~]# echo $i
600

i++和++i区别

# i++和++i的执行结果都是将i自增1
[root@demo-c8 ~]# i=1
[root@demo-c8 ~]# let i++
[root@demo-c8 ~]# echo $i
2
[root@demo-c8 ~]# i=1
[root@demo-c8 ~]# let ++i
[root@demo-c8 ~]# echo $i
2
[13:29:05 root@c8prac ~]#i=1;let j=i++; echo "i=$i,j=$j"     #i++先赋值, 再自增
i=2,j=1
[13:29:12 root@c8prac ~]#unset i j;i=1;let j=++i; echo "i=$i,j=$j" #++i先自增, 再赋值
i=2,j=2

鸡兔同笼:

#!/bin/bash
  
HEAD=$1
FOOT=$2

RABBIT=$[(FOOT-HEAD-HEAD)/2]
CHOOK=$[HEAD-RABBIT]

printf "%-10s %-10s \n" CHOOK:$CHOOK RABBIT:$RABBIT
[13:42:35 root@c8prac ~]#bash chook_rabbit.sh 35 100
CHOOK:20   RABBIT:15  

范例: 计算结果保留3位小数, 默认情况, Shell计算不会保留小数, 只保留整数部分, 也不会四舍五入

[root@demo-c8 ~]# let var=20/3
[root@demo-c8 ~]# echo $var
6
# bc默认也不支持小数, 需要通过scale指定保留几位小数
[root@demo-c8 ~]# echo "scale=3; 20/3" | bc
6.666

2.10 逻辑运算

逻辑运算中的0和1都是二进制数, 表示真假, 而$?退出状态码的0-255是十进制数, 0表示上一条命令执行成功, 非0表示上一条命令执行失败, 两者不同

真;假

true,false
1;0 

与运算: & 任何数与0相与, 结果为0, 任何数与1相与, 结果为原值

1 与 1 = 1
1 与 0 = 0
0 与 1 = 0
0 与 0 = 0

范例: 十进制数与运算

# 十进制数与运算, 需要把十进制转换成二进制, 之后, 针对每一位二进制数进行与运算, 再将最后的结果转为十进制

[root@demo-c8 data]# let var=8\&8
[root@demo-c8 data]# echo $var
8
[root@demo-c8 data]# let var=8\&4
[root@demo-c8 data]# echo $var
0
[root@demo-c8 data]# let var=1\&1
[root@demo-c8 data]# echo $var
1
[root@demo-c8 data]# let var=1\&0
[root@demo-c8 data]# echo $var
0

或运算: | 任何数和1或, 结果为1, 和0或结果为原值

1 或 1 = 1
1 或 0 = 1
0 或 1 = 1
0 或 0 = 0

范例: 十进制数或运算

[root@demo-c8 data]# echo $((8|4))
12

非运算: !

! 1 = 0   ! true=false
! 0 = 1 ! false=true

异或运算: 相同为假, 不同为真

异或的两个值,相同为假,不同为真。两个数字X,Y异或得到结果Z,Z再和任意两者之一X异或,将得出另一个值Y

1 ^ 1 = 0
1 ^ 0 = 1
0 ^ 1 = 1
0 ^ 0 = 0
x^y=z
x^z=y
y^z=x

范例: 异或运算实现变量互换

[13:42:36 root@c8prac ~]#x=1;y=2;x=$[x^y];y=$[x^y];x=$[x^y];echo x=$x,y=$y
x=2,y=1
[23:07:24 root@centos-7-1 ~]#x=10;y=20; temp=$x; x=$y; y=$temp; echo $x, $y
20, 10

短路运算

  • 短路与
CMD1 短路与 CMD2
第一个CMD1结果为真 (1),第二个CMD2必须要参与运算,才能得到最终的结果
第一个CMD1结果为假 (0),总的结果必定为0,因此不需要执行CMD2
  • 短路或
CMD1 短路或 CMD2
第一个CMD1结果为真 (1),总的结果必定为1,因此不需要执行CMD2
第一个CMD1结果为假 (0),第二个CMD2 必须要参与运算,才能得到最终的结果

2.11 条件测试命令

条件测试: 判断某需求是否满足, 需要由测试机制来实现, 专用的测试表达式需要由测试命令辅助完成测试过程, 实现评估布尔声明, 以便用在条件性环境下进行执行

测试结果为真, 则状态码变量$?返回0
测试结果为假, 则状态码变量$?返回1

条件测试命令:

# 表达式前后必须有空白字符, 也就是空格
test 表达式
[] 表达式; 等价于test, 建议用[]
[[]] 表达式; 功能强于test和[]
# 查看条件测试命令帮助

help test
help [

2.11.1 变量测试

判断变量是否已经定义

[ -v VARNAME ]

[00:15:25 root@c7node2 ~]#[ -v NAME ]
[00:19:03 root@c7node2 ~]#echo $?
1
[00:19:05 root@c7node2 ~]#NAME=WANG
[00:19:12 root@c7node2 ~]#[ -v NAME ]
[00:19:14 root@c7node2 ~]#echo $?
0
# 如果变量定义了, 但是没赋值, 那么也返回0

[root@demo-c8 data]# unset X
[root@demo-c8 data]# X=
[root@demo-c8 data]# [ -v X ]
[root@demo-c8 data]# echo $?
0

2.11.2 数值测试

-eq    等于
-ne    不等于
-lt    小于
-le    小于等于
-gt    大于
-ge    大于等于
[root@demo-c8 data]# i=1;j=2
[root@demo-c8 data]# [[ $i -eq $j ]]
[root@demo-c8 data]# echo $?
1
[root@demo-c8 data]# i=100;j=200
[root@demo-c8 data]# [ $i -gt $j ]
[root@demo-c8 data]# echo $?
1
[00:22:18 root@c7node2 ~]#x=1;y=2
[00:22:24 root@c7node2 ~]#[ $x -eq $y ]  # 条件判断中, 变量引用要加$
[00:22:26 root@c7node2 ~]#echo $?
1

算数表达式比较:

<= 
>= 
< 
>
[00:22:28 root@c7node2 ~]#x=1;y=2;(( x > y ));echo $?  # 如果用(())进行条件运算, 变量不用加$, 加上也可以
1

2.11.3 字符串测试

test和[]用法

test和 [ ]用法
-z STRING 字符串是否为空,没定义或空为真, $?返回0,不空为假$?返回1
-n STRING 字符串是否不空,不空为真,空为假
   STRING   同上
STRING1 = STRING2 是否等于,注意 = 前后有空格
STRING1 != STRING2 是否不等于
> 比较ascii码编号
< 比较ascii码编号

范例: 判断字符串是否为空, 也可判断变量是否被赋值了

# 判断是否非空, 如果有值, 那么就是非空的, 返回0
[00:29:26 root@c7node2 ~]#NAME=WANG;[ $NAME ]; echo $?
0
# 如果没有值, 那么就是空的, 返回1
[00:29:42 root@c7node2 ~]#NAME=WANG;unset NAME;[ $NAME ]; echo $?
1
[00:29:56 root@c7node2 ~]#[ "" ];echo $?  #双引号里面为空, 则结果为假. 判断的是双引号里面的东西, 而不是双引号本身
1

[root@demo-c8 data]# [ -z " " ]; echo $? # 双引号内是空格, 非空, 所以结果为假
1

范例: 判断字符串是否相等

       STRING1 = STRING2  # 字符串和括号之间要有空格
              the strings are equal
[00:34:46 root@c7node2 ~]#NAME1=WANG;NAME2=LI;[ $NAME1 = $NAME2 ];echo $?
1
[00:35:57 root@c7node2 ~]#NAME1=WANG;NAME2=LI;[ $NAME1 != $NAME2 ];echo $?
0  # != 不等

范例: 在比较字符串时, 建议变量用双引号括起来

[root@demo-c8 data]# NAME="I love Linux"
[root@demo-c8 data]# [ $NAME ]
-bash: [: love: binary operator expected
[root@demo-c8 data]# [ "$NAME" ]
[root@demo-c8 data]# echo $?
0

[[]]用法

[[ expression ]] 用法
==: 左侧字符串是否和右侧的PATTERN相同
    注意: 此表达式用于[[]]中, PATTERN为通配符

=~: 左侧字符串是否能够被右侧的扩展正则表达式PATTERN所匹配
    注意: 此表达式用于[[]]中, 是扩展正则表达式


建议: 当使用正则表达式或者通配符时使用[[]], 其他情况一般使用[]

举例:

PATTERN为通配符: 此时. 如果*作为通配符, 那么不用加"", 如果想作为*本身, 那么就加""或者转义\

[root@demo-c8 data]# FILE="linux10"
[root@demo-c8 data]# [[ "$FILE" == linux* ]] # 不加双引号, 那么*就是通配符
[root@demo-c8 data]# echo $?
0
[root@demo-c8 data]# [[ "$FILE" == "linux*" ]] # 加上双引号, 那么*就是本身意思了, 不是通配符了
[root@demo-c8 data]# echo $?
1
[00:36:04 root@c7node2 ~]#FILE=backup.sh
[00:45:40 root@c7node2 ~]#[[  $FILE == *.sh ]];echo $?
0
[00:46:03 root@c7node2 ~]#[[  $FILE != *.sh ]];echo $?
1

[root@demo-c8 data]# FILE=test.log
[root@demo-c8 data]# [[ "$FILE"==*.log ]]
[root@demo-c8 data]# echo $?
0

[root@demo-c8 data]# [[ "$FILE" == *.sh ]]
[root@demo-c8 data]# echo $?
0

[root@demo-c8 data]# FILE=test.zip
[root@demo-c8 data]# [[ "$FILE" != *.sh ]]
[root@demo-c8 data]# echo $?
0

PATTERN正则表达式: 此时, PATTERN不用加双引号

[00:46:24 root@c7node2 ~]#FILE=test.log
[00:47:37 root@c7node2 ~]#[[ $FILE  =~ \.log$  ]];echo $?  # 判断字符串是否以.log结尾
0

[root@demo-c8 data]# FILE="test.sh"
[root@demo-c8 data]# [[ "$FILE" =~ \.sh$ ]]
[root@demo-c8 data]# echo $?
0
[root@demo-c8 data]# [[ "$N" =~ ^[0-9]+$ ]]
[root@demo-c8 data]# echo $?
0
[root@demo-c8 data]# M="Linux10"
[root@demo-c8 data]# [[ "$M" =~ ^[0-9]+$ ]]
[root@demo-c8 data]# echo $?
1

注意: 比较字符串时, 建议用双引号引起来, 把字符串作为一个整体 否则容易出错

举例:

[00:48:09 root@c7node2 ~]#NAME="I love Linux"
[00:54:07 root@c7node2 ~]#[ $NAME ]  # 相当于执行[ I love Linux ], 当做命令执行了
-bash: [: love: binary operator expected
[00:54:11 root@c7node2 ~]#[ "$NAME" ] # 相当于执行[ "I love Linux" ], 进行字符串判断
[00:54:16 root@c7node2 ~]#echo $?
0

范例: 判断一个地址是否为ip地址


2.11.4 文件测试

针对不同的文件类型是否存在, 以及是否为存在的目录进行判断

存在性测试:

-a | -e FILE: 判断文件或者目录是否存在
-b FILE: 判断文件是否存在且为块设备
-c FILE: 判断文件是否存在且为字符设备文件
-d FILE: 判断文件是否存在且为目录文件
-f FILE: 判断文件是否存在且为普通文件
-h | -L: 判读文件是否存在且为符号链接文件
-p FILE: 判断文件是否存在且为命令管道文件
-S FILE: 判断文件是否存在且为套接字文件
[00:54:19 root@c7node2 ~]#[ -e /etc/issue ]; echo $? 
0
[01:04:20 root@c7node2 ~]#[ -d /etc ]; echo $? 
0
[01:04:34 root@c7node2 ~]#[ -h /bin ]; echo $? 
0

文件权限测试:

-r FILE   是否存在且可读
-w FILE  是否存在且可写
-x  FILE 是否存在且可执行
-u FILE 是否存在且拥有suid权限
-g FILE 是否存在且拥有sgid权限
-k FILE 是否存在且拥有sticky权限

注意: 最终结果由用户对文件的实际权限决定, 而非文件本身属性决定

举例: shadow文件用于保存用户密码, 而passwd程序拥有SUID权限, 所以任何用户都是可以继承root用户权限, 去修改和查看shadow文件内容

[01:07:40 root@c7node2 ~]#[ -w /etc/shadow ]; echo $?
0
[01:07:46 root@c7node2 ~]#[ -x /etc/shadow ]; echo $?  # x权限必须有x位才行, 即使root账号没有x权限, 也无法执行文件
1
[01:07:48 root@c7node2 ~]#[ -r /etc/shadow ]; echo $?
0

范例: 给文件添加了特殊只读权限, 那么即使文件本身是可写的, 那么也会变成无法写

[root@demo-c8 data]# [ -w /etc/fstab ]
[root@demo-c8 data]# echo $?
0
[root@demo-c8 data]# chattr +i /etc/fstab
[root@demo-c8 data]# lsattr /etc/fstab 
----i--------------- /etc/fstab
[root@demo-c8 data]# echo 1 >> /etc/fstab
-bash: /etc/fstab: Operation not permitted
[root@demo-c8 data]# [ -w /etc/fstab ] 
[root@demo-c8 data]# echo $?
1

文件属性测试:

-s FILE #是否存在且非空
-t fd #fd 文件描述符是否在某终端已经打开
-N FILE #文件自从上一次被读取之后是否被修改过
-O FILE #当前有效用户是否为文件属主
-G FILE #当前有效用户是否为文件属组
FILE1 -ef FILE2 #FILE1是否是FILE2的硬链接
FILE1 -nt FILE2 #FILE1是否新于FILE2(mtime)
FILE1 -ot FILE2 #FILE1是否旧于FILE2

2.12 关于() vs {}

(CMD1;CMD2;...){ CMD1;CMD2;...; } 都可以将多个命令组合在一起,批量执行

(CMD1;CMD2;...): 开启子进程, 变量赋值和内部命令不会影响当前环境, 只在子进程生效; 并且小括号里的子进程是可以从其父进程继承普通变量的. 小括号和命令之间无需空格

[root@demo-c8 ~]# name="admin";echo $BASHPID;(echo $BASHPID;echo ${name};);echo $BASHPID
3662
6854
admin  # 子进程6854从父进程3662进程name变量
3662

{ CMD1;CMD2;...; }: 在当前Shell中执行命令, 不开启子进程. 会影响当前Shell环境. 花括号和命令之间要有空格

[root@demo-c8 ~]# name="root";echo $BASHPID;{ echo $BASHPID;name="david";echo ${name}; };echo $BASHPID; echo ${name}
3662
3662
david
3662
david

举例:

[01:18:17 root@c7node2 ~]#hostname;pwd > test.log # 分号只会把pwd这个命令的结果追加到test.log
c7node2.linux
[01:18:26 root@c7node2 ~]#cat test.log 
/root
[01:18:29 root@c7node2 ~]#{ hostname;pwd; } > test.log # 通过{}将两个命令作为整体一起执行, 把两个命令的结果都追加给test.log
[01:18:41 root@c7node2 ~]#cat test.log 
c7node2.linux
/root

()的实用案例:

范例: 开启子进程, 临时切换目录执行命令, 不改变当前工作目录

[root@demo-c8 ~]# (cd /data;ls;) # 小括号会开启子进程, 在子进程内切换目录, 在父进程中,还是停留在源目录
rm.sh  test.sh
[root@demo-c8 ~]# pwd
/root
[root@demo-c8 ~]# { cd /data;ls; } # 花括号不会开启子进程, 会在当前进程切换目录, 命令执行结束后, 会切换到新的目录
rm.sh  test.sh
[root@demo-c8 data]# pwd
/data

范例: 临时修改umask, 创建文件

[root@demo-c8 ~]# umask
0022
[root@demo-c8 ~]# (umask 000; touch f1.txt)  # 666-000=666
[root@demo-c8 ~]# umask
0022
[root@demo-c8 ~]# ll f1.txt 
-rw-rw-rw-. 1 root root 0 Sep  8 19:51 f1.txt

2.13 组合条件测试

2.13.1第一种方式: []

[ EXPRESSION1 -a EXPRESSION2 ] 并且, 1和2都为真, 结果才为真
[ EXPRESSION1 -o EXPRESSION2 ] 或者, 1和2只要有一个真, 结果就为真
[ !  EXPRESSION ] 取反

说明: -a 和 -o 需要使用条件测试命令(test[])进行,[[ ]] 不支持

举例:

# test.log是否为普通文件, 并且当前用户对于该文件是否有执行权限
[01:28:48 root@c7node2 ~]#[ -f test.log -a -x test.log  ] 
[01:29:05 root@c7node2 ~]#echo $?
1
[01:29:09 root@c7node2 ~]#[ -f test.log -o -x test.log  ] 
[01:29:41 root@c7node2 ~]#echo $?
0
[root@demo-c8 ~]# ll test.log 
-rw-r--r--. 1 root root 6 Sep  8 19:43 test.log
[root@demo-c8 ~]# [ ! -x test.log ] 
[root@demo-c8 ~]# echo $?
0

2.13.2 第二种方式: &&和||

短路与  &&
短路或  ||
! CMD 取反

CMD1 && CMD2 || CMD3: &&要在前面使用

CMD1如果执行失败, 则不执行CMD2,  此时CMD1 && CMD2作为整体已经为假, 则继续执行CMD3
如果CMD1执行成功, 则执行CMD2
[root@demo-c8 ~]# [ -f /bin/cat -a -x /bin/cat ] && cat /etc/fstab 

#
# /etc/fstab
# Created by anaconda on Mon Aug 15 16:52:19 2022
#
# Accessible filesystems, by reference, are maintained under '/dev/disk/'.
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info.
#
# After editing this file, run 'systemctl daemon-reload' to update systemd
# units generated from this file.
#
UUID=b1ab1ace-2582-4afd-8693-39bd9855041c /                       xfs     defaults        0 0
UUID=d5131695-82b3-4a23-bc28-5c8a4bf381a0 /boot                   ext4    defaults        1 2
UUID=bdd66510-e510-4fe7-ba71-e2a35e6dc492 /data                   xfs     defaults        0 0
UUID=05c944fb-d6f9-4544-ba10-8b7bf3cc8fed swap                    swap    defaults        0 0

范例: 如果生成的随机数能被6整除, 那么返回"yes", 否则返回"no"

[root@demo-c8 ~]# [ $[$RANDOM%6] -eq 0 ] && echo "yes" || echo "no"
no
[root@demo-c8 ~]# [ $[$RANDOM%6] -eq 0 ] && echo "yes" || echo "no"
no
[root@demo-c8 ~]# [ $[$RANDOM%6] -eq 0 ] && echo "yes" || echo "no"
yes
[root@demo-c8 ~]# [ $[$RANDOM%6] -eq 0 ] && echo "yes" || echo "no"
yes
[root@demo-c8 ~]# [ $[$RANDOM%6] -eq 0 ] && echo "yes" || echo "no"
no
[root@demo-c8 ~]# [ $[$RANDOM%6] -eq 0 ] && echo "yes" || echo "no"
no
[root@demo-c8 ~]# [ $[$RANDOM%6] -eq 0 ] && echo "yes" || echo "no"
no

范例: 简单的网站健康性检查

curl 127.0.0.1 &> /dev/null && echo "Host running" || { echo "Host down"; systemctl restart nginx; }
Host down

范例: 网络状态判断

#!/bin/bash

for i in {1..254};do
ping -c1 -W1 10.0.0.$i &> /dev/null && echo "10.0.0.$i is up" &> ok.log || echo "10.0.0.$i is down" &> down.log;
done

范例: 磁盘空间利用率判断

#!/bin/bash

WARNING=80
DISK=`df -h | sed -nr 's#(^\/dev/sda[0-9]{1})(.*)#\1#p'`

for i in $DISK;do
[ `df -h | grep "$i" | tr -s ' ' | cut -d ' ' -f 5 | cut -d '%' -f1` -eq $WARNING ] && echo "$i is bad" || echo "$i is good";
done

范例:磁盘空间和Inode号的检查脚本

[root@centos8 scripts]#cat disk_check.sh
#!/bin/bash
WARNING=80
SPACE_USED=`df | grep '^/dev/sd'|grep -oE '[0-9]+%'|tr -d %| sort -nr|head -1`
INODE_USED=`df -i | grep '^/dev/sd'|grep -oE '[0-9]+%'|tr -d %| sort -nr|head -1`
[ "$SPACE_USED" -gt $WARNING -o "$INODE_USED" -gt $WARNING ] && echo "DISK USED:$SPACE_USED%, INODE_USED:$INODE_USED,will be full" | mail -s "DISK Warning" [email protected]

练习
1、编写脚本 argsnum.sh,接受一个文件路径作为参数;如果参数个数小于1,则提示用户“至少应该给一个参数”,并立即退出;如果参数个数不小于1,则显示第一个参数所指向的文件中的空白行数

2、编写脚本 hostping.sh,接受一个主机的IPv4地址做为参数,测试是否可连通。如果能ping通,则提示用户“该IP地址可访问”;如果不可ping通,则提示用户“该IP地址不可访问”
3、编写脚本 checkdisk.sh,检查磁盘分区空间和inode使用率,如果超过80%,就发广播警告空间即将满了

4、编写脚本 per.sh,判断当前用户对指定参数文件,是否不可读并且不可写

5、编写脚本 excute.sh ,判断参数文件是否为sh后缀的普通文件,如果是,添加所有人可执行权限,否则提示用户非脚本文件

6、编写脚本 nologin.sh和 login.sh,实现禁止和允许普通用户登录系统

2.13 使用read命令来接受输入

使用read来把输入值分配给一个或多个Shell变量, read从标准输入中读取值, 给每个单词分配一个变量, 所有剩余单词, 都会被分配给最后一个变量, 如果变量名没有指定, 默认标准输入的值会赋值给系统内置变量REPLY

格式:

read [options] [name ...]

常见选项:

-p   指定要显示的提示
-s   静默输入,一般用于密码
-n N 指定输入的字符长度N
-d '字符'   输入结束符
-t N TIMEOUT为N秒

范例: 将标准输入存到REPLY变量

image.png

范例: 将标准输入存到指定的变量

image.png
image.png

范例: 按照提示输入信息

image.png

范例: 利用标准输入, 执行read

# 标准输入需要用<<<
[root@demo-c8 ~]# read x y z <<< "l love linux"
[root@demo-c8 ~]# echo $x $y $z
l love linux
# 通过管道接收标准输入
[root@demo-c8 ~]# unset NAME
[root@demo-c8 ~]# echo "admin" | read NAME
[root@demo-c8 ~]# echo $NAME
# 管道会开启两个子进程, 管道前后的命令是运行在不同的子进程的, 而echo $NAME运行在父进程, 所以在父进程是拿不到read子进程的NAME变量值的
[root@demo-c8 ~]# echo "admin" | read NAME; echo $NAME
# 通过{}或者[]确保read和echo处在同一个进程
[root@demo-c8 ~]# echo "admin" | { read NAME; echo $NAME; }
admin
[root@demo-c8 ~]# echo "root" | (read NAME;echo $NAME;)
root

范例: 管道开启两个子进程

[root@demo-c8 opt]# echo $BASHPID
2099
image.png
image.png

范例: 在管道后, 接收并返回管道前传入的值

[root@demo-c8 opt]# echo 1 2 | read x y 
[root@demo-c8 opt]# echo $x

[root@demo-c8 opt]# echo $y

[root@demo-c8 opt]# 
  • read x y和echo $x; echo $y分开写, 也是处在不同的进程
image.png
[root@demo-c8 opt]# echo 1 2 | (read x y; echo $x; echo $y) 
1
2
[root@demo-c8 opt]# echo 1 2 | { read x y; echo $x; echo $y; } 
1
2

范例: 将变量定义存放到文件中, 之后通过标准输入, 传给read

[root@demo-c8 ~]# vim test.txt
1 2

[root@demo-c8 ~]# read i j < test.txt 
[root@demo-c8 ~]# echo $i
1
[root@demo-c8 ~]# echo $j
2

范例: 根据用户输入执行不同命令

[root@demo-c8 data]# vim read.sh

[root@demo-c8 data]# vim read.sh

#!/bin/bash
  
read -p "请输入是否执行(y|Y for yes; other keys for no): " ACTION
[[ $ACTION =~ ^[Yy]$ ]] && echo "action" || echo "no action"

[root@demo-c8 data]# bash read.sh 
请输入是否执行(y|Y for yes; other keys for no): y
action
[root@demo-c8 data]# bash read.sh 
请输入是否执行(y|Y for yes; other keys for no): Y
action
[root@demo-c8 data]# bash read.sh 
请输入是否执行(y|Y for yes; other keys for no): n
no action
[root@demo-c8 data]# bash read.sh 
请输入是否执行(y|Y for yes; other keys for no): N
no action
[root@demo-c8 data]# bash read.sh 
请输入是否执行(y|Y for yes; other keys for no): adad
no action

范例: 实现运维脚本的菜单功能

[root@demo-c8 data]# vim devops.sh 

#!/bin/bash
. /etc/init.d/functions

echo -en "\033[$[$RANDOM%7+31];1m"

cat <
image.png

你可能感兴趣的:(7: Shell脚本基础)