# Shell脚本编程
# 简介
- Shell 是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁。Shell 既是一种命令语言,又是一种程序设计语言。
- Shell 是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。(翻译官,帮你翻译命令给内核执行)
- Linux 的 Shell 种类众多,常见的有:
- Bourne Shell(/usr/bin/sh或/bin/sh)
- Bourne Again Shell(/bin/bash)
- C Shell(/usr/bin/csh)
- K Shell(/usr/bin/ksh)
- Shell for Root(/sbin/sh)
- 程序编程风格
- 过程式:以指令为中心,数据服务于命令
- 对象式:以数据为中心,命令服务于数据
- shell是一种过程式编程
- 过程式编程
- 顺序执行
- 循环执行
- 选择执行
- 编程语言分类
- 编译型语言
- 解释型语言(shell是一种解释型语言)
- 运行脚本
- 给予执行权限,通过具体的文件路径指定文件执行
- 直接运行解释器,将脚本作为解释器程序的参数运行
- bash退出状态码
- 范围是0-255
- 脚本中一旦遇到exit命令,脚本会立即终止,终止退出状态取决于exit命令后面的数字
- 如果未给脚本指定退出状态码,整个脚本的退出状态码取决于脚本中执行的最后一条命令的状态
# 变量
## 变量命名
- 命名只能使用英文字母,数字和下划线,首个字符不能以数字开头。
- 中间不能有空格,可以使用下划线(_)。
- 不能使用标点符号。
- 不能使用 bash 里的关键字(可用 help 命令查看保留关键字)。
## 声明变量
访问变量的语法形式为:`${var}` 和 `$var` 。
变量名外面的花括号是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界,所以推荐加花括号。
```bash
#!/bin/bash
word="hello"
echo ${word}
# Output: hello
```
## 只读变量
使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变。
```
#!/bin/bash
rword="hello"
echo ${rword}
readonly rword
# rword="bye" # 如果放开注释,执行时会报错
```
## 删除变量
```bash
dword="hello" # 声明变量
echo ${dword} # 输出变量值
# Output: hello
unset dword # 删除变量
echo ${dword}
# Output: (空)
```
## 变量类型
- **局部变量** - 局部变量是仅在某个脚本内部有效的变量。它们不能被其他的程序和脚本访问。
- **环境变量** - 环境变量是对当前 shell 会话内所有的程序或脚本都可见的变量。创建它们跟创建局部变量类似,但使用的是 `export` 关键字,shell 脚本也可以定义环境变量。
**常见的环境变量:**
| 变量 | 描述 |
| --------- | -------------------------------------------------- |
| `$HOME` | 当前用户的用户目录 |
| `$PATH` | 用分号分隔的目录列表,shell 会到这些目录中查找命令 |
| `$PWD` | 当前工作目录 |
| `$RANDOM` | 0 到 32767 之间的整数 |
| `$UID` | 数值类型,当前用户的用户 ID |
| `$PS1` | 主要系统输入提示符 |
| `$PS2` | 次要系统输入提示符 |
- **本地变量** - 生效范围仅为当前shell进程;(其他shell,当前的子sehll进程均无效)
- 变量赋值:name = “value”
- **位置变量** - shell 脚本中用来引用命令行参数的特殊变量。当你运行一个 shell 脚本时,可以在命令行上传递参数,这些参数可以在脚本中使用位置变量引用。
位置变量包括以下几种:
1. `$0`: 表示脚本本身的名称。
2. `$1`, `$2`, `$3`, ..., `$n`: 分别表示第1个、第2个、第3个...第n个参数。
3. `$#`: 表示传递给脚本的参数个数。
4. `$*`: 表示所有参数,将所有参数当作一个整体。
5. `$@`: 表示所有参数,但是每个参数都是独立的。
```bash
[root@localhost ~]# cat hello.sh
#!/bin/bash
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "Total arguments: $#"
echo "All arguments: $*"
echo "All arguments (separately): $@"
[root@localhost ~]# ./hello.sh world 2023
```
案例:统计给出指定文件的行数
```bash
[root@localhost ~]# cat hello.sh
#!/bin/bash
linecount="$(wc -l /etc/passwd | awk -F" " '{print $1}')"
echo "This file have ${linecount} lines"
[root@localhost ~]# bash hello.sh
This file have 21 lines
```
# 字符串
shell 字符串可以用单引号 `' '`,也可以用双引号 `" "`,也可以不用引号。
- 单引号的特点
- 单引号里不识别变量
- 单引号里不能出现单独的单引号(使用转义符也不行),但可成对出现,作为字符串拼接使用。
- 双引号的特点
- 双引号里识别变量
- 双引号里可以出现转义字符
综上,推荐使用双引号。
## 字符串的拼接
```bash
# 使用单引号拼接
name1='white'
str1='hello, '${name1}''
str2='hello, ${name1}'
echo ${str1}_${str2}
# Output:
# hello, white_hello, ${name1}
# 使用双引号拼接
name2="black"
str3="hello, "${name2}""
str4="hello, ${name2}"
echo ${str3}_${str4}
# Output:
# hello, black_hello, black
```
## 获取字符串的长度
```bash
text="12345"
echo ${#text}
# Output:
# 5
```
## 截取子字符串
`${variable:start:length}`
```bash
text="12345"
echo ${text:2:2}
# Output:
# 34
```
# 数组
bash 只支持一维数组。
数组下标从 0 开始,下标可以是整数或算术表达式,其值应大于或等于 0。
## 创建/访问数组
```bash
array_name=(value1 value2 value3 ...)
array_name=([0]=value1 [1]=value2 ...)
# 案例一
[root@localhost ~]# cat a.sh
#!/bin/bash
# 创建数组
fruits=("apple" "banana" "orange")
# 访问元素
echo "First fruit: ${fruits[0]}"
echo "All fruits: ${fruits[@]}"
[root@localhost ~]# bash a.sh
First fruit: apple
All fruits: apple banana orange
# 案例二
[root@localhost ~]# cat a.sh
nums=([0]="nls" [1]="18" [2]="teacher")
echo ${nums[1]}
[root@localhost ~]# bash a.sh
18
```
访问数组中所有的元素:
```bash
[root@localhost ~]# cat a.sh
nums=([0]="nls" [1]="18" [2]="teacher")
echo ${nums[*]}
echo ${nums[@]}
[root@localhost ~]# bash a.sh
nls 18 teacher
nls 18 teacher
```
## 获取数组的长度
```bash
[root@localhost ~]# cat a.sh
nums=([0]="nls" [1]="18" [2]="teacher")
echo "数组元素个数为: ${#nums[*]}"
[root@localhost ~]# bash a.sh
数组元素个数为: 3
```
## 删除元素
用`unset`命令来从数组中删除一个元素:
```bash
[root@localhost ~]# cat a.sh
nums=([0]="nls" [1]="18" [2]="teacher")
echo "数组元素个数为: ${#nums[*]}"
unset nums[0]
echo "数组元素个数为: ${#nums[*]}"
[root@localhost ~]# bash a.sh
数组元素个数为: 3
数组元素个数为: 2
```
# 运算符
## 算数运算符
下表列出了常用的算术运算符,假定变量 x 为 10,变量 y 为 20:
| 运算符 | 说明 | 举例 |
| ------ | --------------------------------------------- | ------------------------------ |
| + | 加法 | `expr $x + $y` 结果为 30。 |
| - | 减法 | `expr $x - $y` 结果为 -10。 |
| * | 乘法 | `expr $x * $y` 结果为 200。 |
| / | 除法 | `expr $y / $x` 结果为 2。 |
| % | 取余 | `expr $y % $x` 结果为 0。 |
| = | 赋值 | `x=$y` 将把变量 y 的值赋给 x。 |
| == | 相等。用于比较两个数字,相同则返回 true。 | `[ $x == $y ]` 返回 false。 |
| != | 不相等。用于比较两个数字,不相同则返回 true。 | `[ $x != $y ]` 返回 true。 |
**注意:**条件表达式要放在方括号之间,并且要有空格,例如: `[$x==$y]` 是错误的,必须写成 `[ $x == $y ]`
**示例:**
- expr本身是一个命令,可以直接进行运算
```bash
x=10
y=20
echo "x=${x}, y=${y}"
val=`expr ${x} + ${y}`
echo "${x} + ${y} = $val"
val=`expr ${x} - ${y}`
echo "${x} - ${y} = $val"
val=`expr ${x} \* ${y}`
echo "${x} * ${y} = $val"
val=`expr ${y} / ${x}`
echo "${y} / ${x} = $val"
val=`expr ${y} % ${x}`
echo "${y} % ${x} = $val"
if [[ ${x} == ${y} ]]
then
echo "${x} = ${y}"
fi
if [[ ${x} != ${y} ]]
then
echo "${x} != ${y}"
fi
# Execute: ./operator-demo.sh
# Output:
# x=10, y=20
# 10 + 20 = 30
# 10 - 20 = -10
# 10 * 20 = 200
# 20 / 10 = 2
# 20 % 10 = 0
# 10 != 20
```
### 案例一:计算ID之和
计算/etc/passwd文件中第10个用户和第15个用户的ID之和
```bash
[root@localhost ~]# cat id.sh
#!/bin/bash
# userid1=$(cat /etc/passwd | sed -n '10p'| awk -F: '{print $3}')
# userid2=$(cat /etc/passwd | sed -n '15p'| awk -F: '{print $3}')
userid1=$(awk -F: '{if (NR==10) print $3}' /etc/passwd)
userid2=$(awk -F: '{if (NR==15) print $3}' /etc/passwd)
userid_sum=$[$userid1 + $userid2]
echo $userid_sum
# Execute:
[root@localhost ~]# bash id.sh
92
```
### 案例二:统计文件数量
统计/etc/,/var/,/usr/目录下有多少目录和文件
```bash
[root@localhost ~]# cat file.sh
#!/bin/bash
sum_etc=$(find /etc | wc -l)
sum_var=$(find /var | wc -l)
sum_usr=$(find /usr | wc -l)
sum=$[$sum_etc + $sum_var + $sum_usr]
echo $sum
# Execute:
[root@localhost ~]# bash file.sh
35686
```
## 关系运算符
关系运算符只支持数字,不支持字符串,除非字符串的值是数字。
下表列出了常用的关系运算符,假定变量 x 为 10,变量 y 为 20:
| 运算符 | 说明 | 举例 |
| ------ | ----------------------------------------------------- | ---------------------------- |
| `-eq` | 检测两个数是否相等,相等返回 true。 | `[ $a -eq $b ]`返回 false。 |
| `-ne` | 检测两个数是否相等,不相等返回 true。 | `[ $a -ne $b ]` 返回 true。 |
| `-gt` | 检测左边的数是否大于右边的,如果是,则返回 true。 | `[ $a -gt $b ]` 返回 false。 |
| `-lt` | 检测左边的数是否小于右边的,如果是,则返回 true。 | `[ $a -lt $b ]` 返回 true。 |
| `-ge` | 检测左边的数是否大于等于右边的,如果是,则返回 true。 | `[ $a -ge $b ]` 返回 false。 |
| `-le` | 检测左边的数是否小于等于右边的,如果是,则返回 true。 | `[ $a -le $b ]`返回 true。 |
**示例:**
```bash
x=10
y=20
echo "x=${x}, y=${y}"
if [[ ${x} -eq ${y} ]]; then
echo "${x} -eq ${y} : x 等于 y"
else
echo "${x} -eq ${y}: x 不等于 y"
fi
if [[ ${x} -ne ${y} ]]; then
echo "${x} -ne ${y}: x 不等于 y"
else
echo "${x} -ne ${y}: x 等于 y"
fi
if [[ ${x} -gt ${y} ]]; then
echo "${x} -gt ${y}: x 大于 y"
else
echo "${x} -gt ${y}: x 不大于 y"
fi
if [[ ${x} -lt ${y} ]]; then
echo "${x} -lt ${y}: x 小于 y"
else
echo "${x} -lt ${y}: x 不小于 y"
fi
if [[ ${x} -ge ${y} ]]; then
echo "${x} -ge ${y}: x 大于或等于 y"
else
echo "${x} -ge ${y}: x 小于 y"
fi
if [[ ${x} -le ${y} ]]; then
echo "${x} -le ${y}: x 小于或等于 y"
else
echo "${x} -le ${y}: x 大于 y"
fi
# Execute: ./operator-demo2.sh
# Output:
# x=10, y=20
# 10 -eq 20: x 不等于 y
# 10 -ne 20: x 不等于 y
# 10 -gt 20: x 不大于 y
# 10 -lt 20: x 小于 y
# 10 -ge 20: x 小于 y
# 10 -le 20: x 小于或等于 y
```
### 案例:猜数字小游戏
```bash
[root@localhost ~]# vim guess.sh
#!/bin/bash
num2=66
while true
do
read -p "请输入你要猜的数字:" num1
if [ $num1 -gt $num2 ];then
echo "你猜大了"
elif [ $num1 -lt $num2 ];then
echo "你猜小了"
else
echo "你猜对了"
break
fi
done
# Execute:
[root@localhost ~]# bash guess.sh
请输入你要猜的数字:60
你猜小了
请输入你要猜的数字:66
你猜对了
```
## 字符串运算符
下表列出了常用的字符串运算符,假定变量 a 为 "abc",变量 b 为 "efg":
| 运算符 | 说明 | 举例 |
| ------ | ------------------------------------------ | -------------------------- |
| `=` | 检测两个字符串是否相等,相等返回 true。 | `[ $a = $b ]` 返回 false。 |
| `!=` | 检测两个字符串是否相等,不相等返回 true。 | `[ $a != $b ]` 返回 true。 |
| `-z` | 检测字符串长度是否为 0,为 0 返回 true。 | `[ -z $a ]` 返回 false。 |
| `-n` | 检测字符串长度是否为 0,不为 0 返回 true。 | `[ -n $a ]` 返回 true。 |
| `str` | 检测字符串是否为空,不为空返回 true。 | `[ $a ]` 返回 true。 |
示例:
```bash
x="abc"
y="xyz"
echo "x=${x}, y=${y}"
if [[ ${x} = ${y} ]]; then
echo "${x} = ${y} : x 等于 y"
else
echo "${x} = ${y}: x 不等于 y"
fi
if [[ ${x} != ${y} ]]; then
echo "${x} != ${y} : x 不等于 y"
else
echo "${x} != ${y}: x 等于 y"
fi
if [[ -z ${x} ]]; then
echo "-z ${x} : 字符串长度为 0"
else
echo "-z ${x} : 字符串长度不为 0"
fi
if [[ -n "${x}" ]]; then
echo "-n ${x} : 字符串长度不为 0"
else
echo "-n ${x} : 字符串长度为 0"
fi
if [[ ${x} ]]; then
echo "${x} : 字符串不为空"
else
echo "${x} : 字符串为空"
fi
# Execute: ./operator-demo5.sh
# Output:
# x=abc, y=xyz
# abc = xyz: x 不等于 y
# abc != xyz : x 不等于 y
# -z abc : 字符串长度不为 0
# -n abc : 字符串长度不为 0
# abc : 字符串不为空
```
## 逻辑运算符
以下介绍 Shell 的逻辑运算符,假定变量 x 为 10,变量 y 为 20:
| 运算符 | 说明 | 举例 |
| ------ | ---------- | ----------------------------------------------- |
| `&&` | 逻辑的 AND | `[[ ${x} -lt 100 && ${y} -gt 100 ]]` 返回 false |
| `||` | 逻辑的 OR | `[[ ${x} -lt 100 && ${y} -gt 100 ]]`返回 true |
示例:
```bash
x=10
y=20
echo "x=${x}, y=${y}"
if [[ ${x} -lt 100 && ${y} -gt 100 ]]
then
echo "${x} -lt 100 && ${y} -gt 100 返回 true"
else
echo "${x} -lt 100 && ${y} -gt 100 返回 false"
fi
if [[ ${x} -lt 100 || ${y} -gt 100 ]]
then
echo "${x} -lt 100 || ${y} -gt 100 返回 true"
else
echo "${x} -lt 100 || ${y} -gt 100 返回 false"
fi
# Execute: ./operator-demo4.sh
# Output:
# x=10, y=20
# 10 -lt 100 && 20 -gt 100 返回 false
# 10 -lt 100 || 20 -gt 100 返回 true
```
## 布尔运算符
下表列出了常用的布尔运算符,假定变量 x 为 10,变量 y 为 20:
| 运算符 | 说明 | 举例 |
| ------ | --------------------------------------------------- | ------------------------------------------ |
| `!` | 非运算,表达式为 true 则返回 false,否则返回 true。 | `[ ! false ]` 返回 true。 |
| `-o` | 或运算,有一个表达式为 true 则返回 true。 | `[ $a -lt 20 -o $b -gt 100 ]` 返回 true。 |
| `-a` | 与运算,两个表达式都为 true 才返回 true。 | `[ $a -lt 20 -a $b -gt 100 ]` 返回 false。 |
示例:
```bash
x=10
y=20
echo "x=${x}, y=${y}"
if [[ ${x} != ${y} ]]; then
echo "${x} != ${y} : x 不等于 y"
else
echo "${x} != ${y}: x 等于 y"
fi
if [[ ${x} -lt 100 && ${y} -gt 15 ]]; then
echo "${x} 小于 100 且 ${y} 大于 15 : 返回 true"
else
echo "${x} 小于 100 且 ${y} 大于 15 : 返回 false"
fi
if [[ ${x} -lt 100 || ${y} -gt 100 ]]; then
echo "${x} 小于 100 或 ${y} 大于 100 : 返回 true"
else
echo "${x} 小于 100 或 ${y} 大于 100 : 返回 false"
fi
if [[ ${x} -lt 5 || ${y} -gt 100 ]]; then
echo "${x} 小于 5 或 ${y} 大于 100 : 返回 true"
else
echo "${x} 小于 5 或 ${y} 大于 100 : 返回 false"
fi
# Execute: ./operator-demo3.sh
# Output:
# x=10, y=20
# 10 != 20 : x 不等于 y
# 10 小于 100 且 20 大于 15 : 返回 true
# 10 小于 100 或 20 大于 100 : 返回 true
# 10 小于 5 或 20 大于 100 : 返回 false
```
## 文件测试运算符
文件测试运算符用于检测 Unix 文件的各种属性。
属性检测描述如下:
| 操作符 | 说明 | 举例 |
| ------- | ------------------------------------------------------------ | --------------------------- |
| -b file | 检测文件是否是块设备文件,如果是,则返回 true。 | `[ -b $file ]` 返回 false。 |
| -c file | 检测文件是否是字符设备文件,如果是,则返回 true。 | `[ -c $file ]` 返回 false。 |
| -d file | 检测文件是否是目录,如果是,则返回 true。 | `[ -d $file ]` 返回 false。 |
| -f file | 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。 | `[ -f $file ]` 返回 true。 |
| -g file | 检测文件是否设置了 SGID 位,如果是,则返回 true。 | `[ -g $file ]` 返回 false。 |
| -k file | 检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true。 | `[ -k $file ]`返回 false。 |
| -p file | 检测文件是否是有名管道,如果是,则返回 true。 | `[ -p $file ]` 返回 false。 |
| -u file | 检测文件是否设置了 SUID 位,如果是,则返回 true。 | `[ -u $file ]` 返回 false。 |
| -r file | 检测文件是否可读,如果是,则返回 true。 | `[ -r $file ]` 返回 true。 |
| -w file | 检测文件是否可写,如果是,则返回 true。 | `[ -w $file ]` 返回 true。 |
| -x file | 检测文件是否可执行,如果是,则返回 true。 | `[ -x $file ]` 返回 true。 |
| -s file | 检测文件是否为空(文件大小是否大于 0),不为空返回 true。 | `[ -s $file ]` 返回 true。 |
| -e file | 检测文件(包括目录)是否存在,如果是,则返回 true。 | `[ -e $file ]` 返回 true。 |
**⌨️ 『示例源码』** [operator-demo6.sh](https://github.com/dunwu/os-tutorial/blob/master/codes/shell/demos/operator/operator-demo6.sh)
```bash
file="/etc/hosts"
if [[ -r ${file} ]]; then
echo "${file} 文件可读"
else
echo "${file} 文件不可读"
fi
if [[ -w ${file} ]]; then
echo "${file} 文件可写"
else
echo "${file} 文件不可写"
fi
if [[ -x ${file} ]]; then
echo "${file} 文件可执行"
else
echo "${file} 文件不可执行"
fi
if [[ -f ${file} ]]; then
echo "${file} 文件为普通文件"
else
echo "${file} 文件为特殊文件"
fi
if [[ -d ${file} ]]; then
echo "${file} 文件是个目录"
else
echo "${file} 文件不是个目录"
fi
if [[ -s ${file} ]]; then
echo "${file} 文件不为空"
else
echo "${file} 文件为空"
fi
if [[ -e ${file} ]]; then
echo "${file} 文件存在"
else
echo "${file} 文件不存在"
fi
# Execute: ./operator-demo6.sh
# Output:(根据文件的实际情况,输出结果可能不同)
# /etc/hosts 文件可读
# /etc/hosts 文件可写
# /etc/hosts 文件不可执行
# /etc/hosts 文件为普通文件
# /etc/hosts 文件不是个目录
# /etc/hosts 文件不为空
# /etc/hosts 文件存在
```
# 用户交互read
## 常用选项
| 选项 | 描述 |
| :--- | :------------------------- |
| `-p` | 在读取输入之前显示提示信息 |
| `-n` | 限制输入的字符数 |
| `-s` | 隐藏用户输入 |
| `-a` | 将输入存储到数组变量中 |
| `-d` | 指定用于终止输入的分隔符 |
| `-t` | 设置超时时间(以秒为单位) |
| `-e` | 允许使用 Readline 编辑键 |
| `-i` | 设置默认值 |
示例:
```bash
#!/bin/bash
read -p "input you name:" name
echo $name
# Output:
nls
```
## 案例:计算器
```bash
#!/bin/bash
echo "Enter the first number:"
read num1
echo "Enter the second number:"
read num2
echo "The sum is: $((num1 + num2))"
echo "The difference is: $((num1 - num2))"
echo "The product is: $((num1 * num2))"
echo "The quotient is: $((num1 / num2))"
# Output:
[root@localhost ~]# bash read.sh
Enter the first number:
10
Enter the second number:
10
The sum is: 20
The difference is: 0
The product is: 100
The quotient is: 1
```
# 控制语句
## 条件语句
跟其它程序设计语言一样,Bash 中的条件语句让我们可以决定一个操作是否被执行。结果取决于一个包在`[[ ]]`里的表达式。
由`[[ ]]`(`sh`中是`[ ]`)包起来的表达式被称作 **检测命令** 或 **基元**。这些表达式帮助我们检测一个条件的结果
1. `if` 语句
`if`在使用上跟其它语言相同。如果中括号里的表达式为真,那么`then`和`fi`之间的代码会被执行。`fi`标志着条件代码块的结束。
```bash
# 写成一行
if [[ 1 -eq 1 ]]; then echo "1 -eq 1 result is: true"; fi
# Output: 1 -eq 1 result is: true
# 写成多行
if [[ "abc" -eq "abc" ]]
then
echo ""abc" -eq "abc" result is: true"
fi
# Output: abc -eq abc result is: true
```
2. `if else` 语句
同样,我们可以使用`if..else`语句,例如:
```bash
if [[ 2 -ne 1 ]]; then
echo "true"
else
echo "false"
fi
# Output: true
```
2. `if elif else` 语句
有些时候,`if..else`不能满足我们的要求。别忘了`if..elif..else`,使用起来也很方便。
```bash
x=10
y=20
if [[ ${x} > ${y} ]]; then
echo "${x} > ${y}"
elif [[ ${x} < ${y} ]]; then
echo "${x} < ${y}"
else
echo "${x} = ${y}"
fi
# Output: 10 < 20
```
## 循环语句
循环其实不足为奇。跟其它程序设计语言一样,bash 中的循环也是只要控制条件为真就一直迭代执行的代码块。Bash 中有四种循环:`for`,`while`,`until`和`select`。
### for循环
`for`与 C 语言中非常像。看起来是这样:
```bash
for arg in elem1 elem2 ... elemN
do
### 语句
done
```
在每次循环的过程中,`arg`依次被赋值为从`elem1`到`elemN`。这些值还可以是通配符或者[大括号扩展](https://github.com/denysdovhan/bash-handbook/blob/master/translations/zh-CN/README.md#大括号扩展)。
当然,我们还可以把`for`循环写在一行,但这要求`do`之前要有一个分号,就像下面这样:
```bash
for i in {1..5}; do echo $i; done
```
还有,如果你觉得`for..in..do`对你来说有点奇怪,那么你也可以像 C 语言那样使用`for`,比如:
```bash
for (( i = 0; i < 10; i++ )); do
echo $i
done
```
当我们想对一个目录下的所有文件做同样的操作时,`for`就很方便了。举个例子,如果我们想把所有的`.bash`文件移动到`script`文件夹中,并给它们可执行权限,我们的脚本可以这样写:
```bash
DIR=/home/zp
for FILE in ${DIR}/*.sh; do
mv "$FILE" "${DIR}/scripts"
done
# 将 /home/zp 目录下所有 sh 文件拷贝到 /home/zp/scripts
```
#### 案例一:创建用户
创建用户user1‐user10家目录,并且在user1‐10家目录下创建1.txt‐10.txt
```bash
[root@localhost ~]# cat adduser.sh
#!/bin/bash
for i in {1..10}
do
mkdir /home/user$i
for j in $(seq 10)
do
touch /home/user$i/$j.txt
done
done
# Output:
[root@localhost ~]# bash adduser.sh
[root@localhost ~]# ls /home/
user01 user10 user3 user5 user7 user9
user1 user2 user4 user6 user8
[root@localhost ~]# ls /home/user1
10.txt 2.txt 4.txt 6.txt 8.txt
1.txt 3.txt 5.txt 7.txt 9.txt
```
#### 案例二:检查磁盘占用
列出/var/目录下各个子目录占用磁盘大小
```bash
[root@localhost ~]# cat size.sh
#!/bin/bash
for i in `ls /var/`
do
path="/var/$i"
if [ -d $path ];then
du -sh $path
fi
done
# Output:
[root@localhost ~]# bash size.sh
0 /var/adm
654M /var/cache
0 /var/crash
8.0K /var/db
0 /var/empty
0 /var/games
0 /var/gopher
0 /var/kerberos
54M /var/lib
0 /var/local
0 /var/lock
3.2M /var/log
0 /var/mail
0 /var/nis
0 /var/opt
0 /var/preserve
0 /var/run
16K /var/spool
0 /var/tmp
0 /var/www
0 /var/yp
```
#### 案例三:测试连通性
批量测试地址是否在线
```bash
[root@localhost ~]# cat ping.sh
#!/bin/bash
for i in {1..10}
do
ping -c 2 192.168.88.$i &> /dev/null
if [ $? -eq 0 ];then
echo 192.168.88.$i >> /root/host.txt
fi
done
# Output:
[root@localhost ~]# cat host.txt
192.168.88.1
192.168.88.2
192.168.88.10
```
### while循环
`while`循环检测一个条件,只要这个条件为 *真*,就执行一段命令。被检测的条件跟`if..then`中使用的[基元](https://github.com/denysdovhan/bash-handbook/blob/master/translations/zh-CN/README.md#基元和组合表达式)并无二异。因此一个`while`循环看起来会是这样:
```bash
while 循环条件
do
### 语句
done
```
#### 案例一:数字累加
计算1+2+..10的总和
```bash
[root@localhost ~]# cat sum.sh
#!/bin/bash
i=1
sum=0
while [ $i -lt 10 ]
do
let sum+=$i
let i++
done
echo $sum
# Output:
[root@localhost ~]# bash sum.sh
45
```
#### 案例二:猜数字小游戏
加上随机数
```bash
[root@localhost ~]# cat guess.sh
#!/bin/bash
num2=$((RANDOM%100+1))
while true
do
read -p "请输入你要猜的数字:" num1
if [ $num1 -gt $num2 ];then
echo "你猜大了"
elif [ $num1 -lt $num2 ];then
echo "你猜小了"
else
echo "你猜对了"
break
fi
done
# Output:
[root@localhost ~]# bash guess.sh
请输入你要猜的数字:50
你猜小了
请输入你要猜的数字:70
你猜小了
请输入你要猜的数字:90
你猜大了
请输入你要猜的数字:80
你猜大了
```
### until循环
`until`循环跟`while`循环正好相反。它跟`while`一样也需要检测一个测试条件,但不同的是,只要该条件为 *假* 就一直执行循环:
```bash
until 条件测试
do
##循环体
done
```
示例:
```bash
[root@localhost ~]# cat until.sh
x=0
until [ ${x} -ge 5 ]; do
echo ${x}
x=`expr ${x} + 1`
done
# Output
[root@localhost ~]# bash until.sh
0
1
2
3
4
```
### 退出循环
`break` 和 `continue`
如果想提前结束一个循环或跳过某次循环执行,可以使用 shell 的`break`和`continue`语句来实现。它们可以在任何循环中使用。
> `break`语句用来提前结束当前循环。
>
> `continue`语句用来跳过某次迭代。
示例:
```bash
# 查找 10 以内第一个能整除 2 和 3 的正整数
i=1
while [[ ${i} -lt 10 ]]; do
if [[ $((i % 3)) -eq 0 ]] && [[ $((i % 2)) -eq 0 ]]; then
echo ${i}
break;
fi
i=`expr ${i} + 1`
done
# Output: 6
```
示例:
```bash
# 打印10以内的奇数
for (( i = 0; i < 10; i ++ )); do
if [[ $((i % 2)) -eq 0 ]]; then
continue;
fi
echo ${i}
done
# Output:
# 1
# 3
# 5
# 7
# 9
```
# 函数
## 函数定义
bash 函数定义语法如下:
```bash
[ function ] funname [()] {
action;
[return int;]
}
```
```bash
function FUNNAME(){
函数体
返回值
}
FUNNME #调用函数
```
> 💡 说明:
>
> 1. 函数定义时,`function` 关键字可有可无。
> 2. 函数返回值 - return 返回函数返回值,返回值类型只能为整数(0-255)。如果不加 return 语句,shell 默认将以最后一条命令的运行结果,作为函数返回值。
> 3. 函数返回值在调用该函数后通过 `$?` 来获得。
> 4. 所有函数在使用前必须定义。这意味着必须将函数放在脚本开始部分,直至 shell 解释器首次发现它时,才可以使用。调用函数仅使用其函数名即可。
示例:
```bash
[root@localhost ~]# cat func.sh
#!/bin/bash
func(){
echo "这是我的第一个函数"
}
echo "------函数执行之前-------"
func
echo "------函数执行之前-------"
# Output:
[root@localhost ~]# bash func.sh
------函数执行之前-------
这是我的第一个函数
------函数执行之前-------
```
## 返回值
示例:
```bash
func(){
echo "这个函数会对输入的两个数字进行相加运算..."
echo "输入第一个数字: "
read aNum
echo "输入第二个数字: "
read anotherNum
echo "两个数字分别为 $aNum 和 $anotherNum !"
return $(($aNum+$anotherNum))
}
func
echo "输入的两个数字之和为 $? !"
#可以使用$?来获取返回值
```
## 函数参数
**位置参数**是在调用一个函数并传给它参数时创建的变量
| 变量 | 描述 |
| ------------------------------------- | ---------------------------- |
| `$0` | 脚本名称 |
| `$1 … $9` | 第 1 个到第 9 个参数列表 |
| `${10} … ${N}` | 第 10 个到 N 个参数列表 |
| `$*` or `$@` | 除了`$0`外的所有位置参数 |
| `$#` | 不包括`$0`在内的位置参数的个数 | |
| `$FUNCNAME` | 函数名称(仅在函数内部有值) |
示例:
```bash
#!/bin/bash
x=0
if [[ -n $1 ]]; then
echo "第一个参数为:$1"
x=$1
else
echo "第一个参数为空"
fi
y=0
if [[ -n $2 ]]; then
echo "第二个参数为:$2"
y=$2
else
echo "第二个参数为空"
fi
paramsFunction(){
echo "函数第一个入参:$1"
echo "函数第二个入参:$2"
}
paramsFunction ${x} ${y}
```
执行结果:
```bash
[root@localhost ~]# vim func1.sh
[root@localhost ~]# bash func1.sh
第一个参数为空
第二个参数为空
函数第一个入参:0
函数第二个入参:0
[root@localhost ~]# bash func1.sh 10 20
第一个参数为:10
第二个参数为:20
函数第一个入参:10
函数第二个入参:20
```
## 函数处理参数
另外,还有几个特殊字符用来处理参数:
| 参数处理 | 说明 |
| -------- | ------------------------------------------------ |
| `$#` | 返回参数个数 |
| `$*` | 返回所有参数 |
| `$ | 参数处理 |
| -------- | ------------------------------------------------ |
| `$!` | 后台运行的最后一个进程的 ID 号 |
| `$@` | 返回所有参数 |
| `$-` | 返回 Shell 使用的当前选项,与 set 命令功能相同。 |
| `$?` | 函数返回值 |
```bash
runner() {
return 0
}
name=zp
paramsFunction(){
echo "函数第一个入参:$1"
echo "函数第二个入参:$2"
echo "传递到脚本的参数个数:$#"
echo "所有参数:"
printf "+ %s\n" "$*"
echo "脚本运行的当前进程 ID 号:$$"
echo "后台运行的最后一个进程的 ID 号:$!"
echo "所有参数:"
printf "+ %s\n" "$@"
echo "Shell 使用的当前选项:$-"
runner
echo "runner 函数的返回值:$?"
}
paramsFunction 1 "abc" "hello, \"zp\""
# Output:
# 函数第一个入参:1
# 函数第二个入参:abc
# 传递到脚本的参数个数:3
# 所有参数:
# + 1 abc hello, "zp"
# 脚本运行的当前进程 ID 号:26400
# 后台运行的最后一个进程的 ID 号:
# 所有参数:
# + 1
# + abc
# + hello, "zp"
# Shell 使用的当前选项:hB
# runner 函数的返回值:0
```
# 实际案例
## 案例一:开机显示系统信息脚本
```bash
[root@localhost ~]# cat os.sh
#!/bin/bash
yum install -y net-tools &> /dev/null
wangka=`ip a | grep ens | head -1 | cut -d: -f2`
System=$(hostnamectl | grep System | awk '{print $3,$4,$5}')
Kernel=$(hostnamectl | grep Kernel | awk -F: '{print $2}')
Virtualization=$(hostnamectl | grep Virtualization| awk '{print $2}')
Statichostname=$(hostnamectl | grep Static|awk -F: '{print $2}')
Ens32=$(ifconfig $wangka | awk 'NR==2 {print $2}')
Lo=$(ifconfig lo0 | awk 'NR==2 {print $2}')
NetworkIp=$(curl -s icanhazip.com)
echo "当前系统版本是:$System"
echo "当前系统内核是:$Kernel"
echo "当前虚拟平台是:$Virtualization"
echo "当前主机名是:$Statichostname"
echo "当前网卡$wangka的地址是:$Ens32"
echo "当前lo0接口的地址是:$Lo"
echo "当前公网地址是:$NetworkIp"
# Output:
[root@localhost ~]# bash os.sh
当前系统版本是:CentOS Linux 7
当前系统内核是: Linux 3.10.0-957.el7.x86_64
当前虚拟平台是:vmware
当前主机名是: localhost
当前网卡 ens33的地址是:192.168.88.10
当前lo0接口的地址是:127.0.0.1
当前公网地址是:153.101.189.87
```
## 案例二:监控httpd进程
**需求:**
1.每隔10s监控httpd的进程数,若进程数大于等于500,则自动重启Apache服务,并检测服务是否重启成功
2.若未成功则需要再次启动,若重启5次依旧没有成功,则向管理员发送告警邮件(使用echo输出已发送即可),并退出检测
3.如果启动成功,则等待1分钟后再次检测httpd进程数,若进程数正常,则恢复正常检测(10s一次),否则放弃重启并向管理员发送告警邮件,并退出检测
```bash
[root@localhost ~]# cat httpd.sh
#!/bin/bash
function check_httpd_process_number() {
process_num=`ps -ef | grep httpd| wc -l`
if [ $process_num -gt 50 ];then
systemctl restart httpd &> /dev/null
# 重启五次httpd确保服务启动
systemctl status httpd &> /dev/null
if [ $? -ne 0 ];then
num_restart_httpd=0
while true;do
let num_restart_httpd++
systemctl restart httpd &> /dev/null
systemctl status httpd &> /dev/null
[ $? -eq 0 ] && break
[ $num_restart_httpd -eq 6 ] && break
done
fi
# 判断重启服务的结果
systemctl status httpd &> /dev/null
[ $? -ne 0 ] && echo "apache未正常重启,已发送邮件给管理员" && return 1
sleep 60
return 0
# 再次判断进程是否正常
process_num=`ps -ef | grep httpd| wc -l`
if [ $process_num -gt 50 ] ;then
echo "apache经过重启进程数依然大于50"
return 1
else
return 0
fi
else
echo "进程数小于50"
sleep 3
return 0
fi
}
# 每十秒钟执行一次函数,检查进程是否正常
while true;do
check_httpd_process_number
[ $? -eq 1 ] && exit
done
# Output:
[root@localhost ~]# bash http.sh
进程数小于50
进程数小于50
进程数小于50
进程数小于50
# 复制窗口进行压力测试
[root@localhost ~]# for i in {1..10}; do ab -c $((10000/$i)) -n 2000 http://127.0.0.1/ & done
```
## 案例三:统计文件
统计两个目录下的相同文件,以及不同文件
```bash
#!/bin/bash
# server1的文件在/test/目录中,server2的文件在/root/demo中,通过md5值来判断文件一致性,最终输出相同文件以及各自的不同文件
#定义两个数组的索引
point1=0
point2=0
echo "/test/的文件:"
# 将server1上的文件的散列值记录到数组当中
for i in `ls /root/demo`;do
md5=`md5sum /root/demo/$i | awk '{print $1}'`
arrar1[$point1]=$md5:$i
echo ${arrar1[$point1]}
let point1++
done
echo "/root/demo的文件:"
# 将server2上的文件的散列值记录到数组当中
for i in `ls /test`;do
md5=`md5sum /test/$i | awk '{print $1}'`
arrar2[$point2]=$md5:$i
echo ${arrar2[$point2]}
let point2++
done
# 找出相同文件以及server1上的独立文件,server1的每个文件都和server2上进行比较
echo "-------------------------------"
for i in ${arrar1[@]};do
for j in ${arrar2[@]};do
temp_flag=0 #定义一个标志位,表示没找到相同的文件
server1_md5=`echo $i | awk -F: '{print $1}'`
server2_md5=`echo $j | awk -F: '{print $1}'`
server1_filename=`echo $i | awk -F: '{print $2}'`
server2_filename=`echo $j | awk -F: '{print $2}'`
if [ $server1_md5 == $server2_md5 ];then
echo -e "两边共同文件\t\t\t$server1_filename"
temp_flag=1 #找到了相同的文件
break
fi
done
if [ $temp_flag -eq 0 ];then
echo -e "server1不同文件\t\t\t$i"
fi
done
# 找出server2上的独立文件
for i in ${arrar2[@]};do
for j in ${arrar1[@]};do
temp_flag=0
server1_md5=`echo $i | awk -F: '{print $1}'`
server2_md5=`echo $j | awk -F: '{print $1}'`
server1_filename=`echo $i | awk -F: '{print $2}'`
server2_filename=`echo $j | awk -F: '{print $2}'`
if [ $server1_md5 == $server2_md5 ];then
temp_flag=1
break
fi
done
if [ $temp_flag -eq 0 ];then
echo -e "server2不同文件\t\t\t$i"
fi
done
```
## 练习:基于文件的用户登录注册功能
用户名和密码保存在文件中,格式为username:password