%%
# 缓冲区笔记
本文件主要记录 shell 脚本"编写"相关的内容。
## Q&A
%%
# shell 脚本
shell 脚本:包含 shell 命令而作为程序执行的文件,以 `.sh` 为扩展名。
shell 脚本中的**首行**用于指定运行该脚本时所使用的 shell,例如 `#!/bin/bash`。
(在 shell 脚本中,`#` 用作注释行,shell 不会处理**除了首行**以外的注释)
#### shell 脚本的运行方式
- (1)**==直接运行脚本==**,脚本**作为可执行文件**直接运行。
- 例如 `$ ./test.sh`,或**将脚本路径添加到 `$PATH` 环境变量中,则可在任意路径下直接用脚本名来运行**。
- 该方式下,当前 shell 将会创建一个子 shell 进程,在该子 shell 中运行该脚本内的命令。
- 该方式下,脚本文件第一行必须**正确声明所使用的解释器(bash 等)**,系统将据此查找。例如 `#!/bin/bash`
- (2)**==启动指定 shell==**,并**将脚本名作为参数传递给该 shell**,**在该 shell 内执行**。
- 例如:`$ bash ./test.sh`, `$ /bin/bash ./test.sh` ;
- 该方式下,**脚本内的首行声明将被忽略**。
- (3)**==使用 `source` 或 `.` 命令运行脚本==**。
- 该方式下,将会**在当前 shell 中**执行脚本内的命令,而不会创建新的子 shell。
- 例如 `source script.sh` 或 `. script.sh`。
- 注意,该方式下,**脚本中定义的任何变量或更改将影响当前的 Shell 会话**。
> [!note] 方式一
> 
> [!note] 方式二
> 
<br><br>
# 解释器文件
"**解释器文件**" 特指 Linux/UNIX 系统上 **以 `#!` 作为首行起始的脚本文件**(文本文件),**执行该文件**时,其将**由==指定的解释器程序==进行执行** [^3]。
解释器文件的首行格式为:`#!<path-name>[optional-argument]`,
其中 `#!` 称之为 "**==Shebang==**" [^1] [^2],用以 **指明执行该脚本的解释器的路径**,即 "**==脚本解释器指示==**"(interpreter directive)。
shell 脚本、python 脚本等,均可通过加入 Shebang 首行而作为解释器文件:
- `#!/bin/bash`,表示使用 `bash` 执行。
- `#!/usr/bin/python3`,表示使用 `python` 解释器执行。
> [!NOTE] 解释器文件本身是 "文本文件",需为其手动设置 "可执行权限"(`chmod +x <file>`)
> [!example] 使用示例
>
> ```Python
> #!/usr/local/bin/python
> import sys
> for arg in reversed(sys.argv[1:]):
> print(arg)
> ```
>
> 可以将 shebang 改为 `#!/usr/bin/env python`,通过 `env` 命令根据 `$PATH` 来搜索 `python` 可执行文件路径并运行 `python`
>
#### 解释器文件的执行原理
- 在 shell 下执行一个解释器文件时(例如 `$ ./run.sh`),shell 会**发起 `execve()` 系统调用**。
- 该系统调用内部在**检测到该文件非 ELF 文件**,而是**文本文件且首行以 `#!` 起始**时,将执行 `fs/binfmt_script.c` 中的解释器加载逻辑,最后实际通过 `exec` 运行的是 "**==解释器程序==**",再由解释器程序来 "解析&执行" 该脚本文件。
<br><br><br>
# 命令替换
shell中的 "命令替换" 用于 **将一个命令的输出作为另一个命令的输入**,包括:
- 将一个命令的输出 **==用作另一个命令的参数==**
- 将一个命令的输出 **==赋值给一个变量==**
> [!NOTE] shell 会创建出**子shell**来运行**指定命令**,因此在子shell中运行命令无法使用父shell脚本中定义的变量
使用命令替换的**两种方式**:
- 使用反引号: `` `command` ``
- 使用`$()` 格式: ` $(command)` 推荐 ✔
示例一:
```shell
#!/bin/bash
# `ls | wc -l` 命令的输出值被赋值给变量 `file_count`;
file_count=`ls | wc -l`
echo "The number of files is $file_count"
# 获取当前日期并以此作为文件名.
today=$(date +%y%m%d)
ls /usr/bin -al > log.$today
```
示例二:
```shell
#!/bin/bash
# `cat $file` 命令的输出用作`for`命令的输入参数
file="states.txt"
for state in $(cat $file)
do
echo "Visit beautiful $state"
done
```
<br><br><br>
# 表达式计算
shell 中支持多种方法进行数学运算:
- **`$((expression))` 语法
- **`bc` 命令**
- **`expr` 命令**
- `let` 命令
> [!NOTE]
> **bash shell 内的数学运算符只支持==整数运算==**。
> 如果要进行浮点数运算,需要使用内置的**bash 计算器 bc**。
>
> 推荐使用 `$((expr))` 进行整数运算,使用 `bc` 进行更复杂的或浮点数运算,
> 不使用 `expr` 命令和 `let` 命令。
<br>
### `$((expr))` 语法
`$((expr))` 是 bash 等现代 shell 中支持的语法,用以求值算术表达式,支持基本数学运算。
**`$((expression))` 的表达式中可以直接通过==变量名==使用变量值,无需 `
或 `${}`。**
```shell
# Assign the result of expression to a variable
var1=$((1 + 5))
var2=$((var1 * 2))
((var2++))
echo "The answer for this is $var2"
```
> 老式语法为 `$[expr]` ,**已过时**,在某些现代 shell 中可能不再支持。
> 强烈推荐使用更现代的标准 `$((expr))` 语法,支持复杂的算术表达式,包括嵌套算术运算。
<br>
### `bc` 计算工具
`bc`( Basic Calculator)是一个用于**精确计算**的**计算器语言**,提供了执行**任意精度的数学运算**的能力,允许在命令行中输入浮点数表达式,然后解释并计算该表达式,最后返回结果。
bash 计算器能够识别以下内容:
- 数字(整数和浮点数)
- 变量(简单变量和数组)
- 注释(以 `#`或 C 语言中的`/* */`开始的行)
- 表达式
- 编程语句(比如 if-then 语句)
- 自定义函数
bc 的 **==浮点数运算==** 是通过内建变量 `scale` 控制的,该变量**默认值为 `0`,即计算结果不包含小数位**。在执行浮点运算之前,需要先设置 `scale` 变量的值,指定运算精度,即结果保留的小数点位数。
bc **可在命令行环境中以交互模式**运行,也**用于执行脚本或作为其它 shell 命令的一部分**。
在 Shell 脚本中使用 `bc` 时,通常会配合 `echo` 和管道 `|` 来**传递表达式**,并通过 "**命令替换**" `$()` 将结果赋给变量
示例一:
```shell
!/bin/bash
reulst=$(echo "scale=2; 3/4" | bc)
echo "Result is: $result"
```
在上例中,`scale=2` 设置结果的**小数精度为两位**,表达式 `3/4` 被计算,然后将结果值赋给变量。
示例二:
```shell
#!/bin/bash
var1=100
var2=3.14159
var3=$(echo "scale=4; $var1 / $var3" | bc)
var4=$(echo "scale=4; $var3 * $var2" | bc)
echo "The answer for this is $var3"
```
示例三:使用内联重定向,直接在命令行中重定向数据
```shell
#!/bin/bash
var1=10.46
var2=43.67
var3=33.2
var4=71
# 使用内联重定向, 将多个表达式放在不同行, 再将内容重定向到BC
var5=$(bc << EOF
scale=4
a1 = ($var1 * $var2)
b1 = ($var3 * $var4)
a1 + b1
EOF
)
echo "The final result is $var5"
```
### `expr` 命令
`expr expression` 是一个外部**命令**,用于求值并返回表达式的结果。若要将该命令的结果赋给变量或作为参数**需要以 "命令替换" 的方式进行**,即使用`$(expr expression)`。
注意事项:
- 表达式的各部分需要用空格分开;
- 特殊字符例如乘法 `*` 前必须带有转义符`\`。
```shell
a=$(expr 3 + 5) # 结果为8
b=$(expr $a \* 2) # 结果为16,注意乘号 (*) 前需要加反斜线 (\) 进行转义
```
### `let` 命令
`let var="expression"` 用于执行一个或多个算术表达式并**将结果赋给指定变量**
表达式中的变量名不需要加 `
前缀,但表达式需要用引号包围起来,特别是包含空格的情况下。
```shell
let a="3 + 5" # 结果为8
let b="a * 2" # 结果为16
```
<br><br><br>
# 结构化命令
shell(如bash、zsh等)中的结构化命令提供了**程序流控制功能**,包括条件执行、循环、函数和分组等。
## if-else语句
**`if`与 `elif` 语句之后默认只能跟"==命令=="**,根据**该命令的==退出状态码==来进行判断**:
- 如果**为0,则表示 true**,then 之后的语句将被执行;
- 如果**非0,则表示 false**。
如果需要在 `if` 语句中使用判断 "**条件**"(而不是命令),有以下两种方式:
- **搭配 `test` 命令**:
- 当 **`test` 命令中列出的条件成立时**,`test` 命令将返回退出状态码0,
- 否则返回非0的退出状态码(命令为空时也返回非0退出状态码)。
- ==**使用方括号== `[ ]`**:方括号定义了**测试条件**,作用与 `test` 命令相同。
- **第一个方括号之后**和**第二个方括号之前**必须留有空格,否则报错。
`test`命令和 `[]` 命令支持判断三类比较条件:**==数值比较==、==字符串比较==、==文件比较==**。
`if` 语句说明示例:
```shell
if command
then commands
fi
# 等效于
if command; then
commands
fi
```
`if` 语句与其后的 `then` 语句**默认必须分别放在两行**。
通过将分号 `;` 放在 if 语句尾部,可以将 then 语句写在同一行。
### 使用说明
if-elif-else语句
```shell
if command1
then
# commands if command1 is true
elif command2
# commands if command2 is true
else
# commands if command2 is false
fi
```
嵌套if语句:
```shell
#!bin/bash
testuser=NoSuchUser
if grep $testuser /etc/passwd
then
echo "The user $testuser account exists on this system."
echo
then
echo "The user $testuser dose not exist on this system."
if ls -d /home/$testuser/
then
echo "However, $testuser has a directory."
fi
fi
echo "We are outside the nested if statements."
```
`if` 语句中使用判断"条件":搭配 `test` 命令或使用方括号 `[ ]`
```shell
# 搭配test命令来使用判断条件
my_variable="Full"
if test $my_variable
then
echo "No expression returns a True"
else
echo "No expression returns a False"
fi
# ------------------------------------------
# 搭配[]来定义判断条件
if [$my_variable]
then
echo "No expression returns a True"
else
echo "No expression returns a False"
fi
```
<br><br>
## 比较条件
"test 命令"和"`[]`" 命令支持三类比较条件:**数值比较、字符串比较、文件比较**
进行比较时,**==变量应该被双引号包围==**(如 `"$a"` 和 `"$b"`),以防止**空变量** 或 **包含空格的变量值** 导致的脚本错误。
示例一:下例中,**即使变量 `b` 是空值**,脚本也不会因为缺少操作数而出错。
```shell
a=10
b=""
if [ "$a" -eq "$b" ]; then
echo "a equals b"
else
echo "a does not equal b"
fi
```
示例二:下例中,由于目录名和文件名可能包含空格, 因此应当将变量名放入双引号内,否则可能引发错误
```shell
for file in /home/rich/test/*
do
if [ -d "$file" ]; then
echo "$file is a directory"
elif [ -f "$file" ]; then
echo "$file is a file"
fi
done
```
### 数值比较
#### 数值比较操作符:
- `-eq`:等于(equal)
- `-ne`:不等于(not equal)
- `-gt`:大于(greater than)
- `-ge`:大于等于(greater than or equal)
- `-lt`:小于(less than)
- `-le`:小于等于(less than or equal)
这些比较操作符**仅适用于整数**。对于浮点数的比较,需要使用其他工具如 `awk` 或 `bc`。
示例:
```shell
a=10
b=20
if [ "$a" -eq "$b" ]; then # 检查$a > $b
echo "a is equal to b"
elif [ "$a" -lt "$b" ]; then
echo "a is less than b"
elif [ "$a" -gt "$b" ]; then
echo "a is greater than b"
fi
```
### 字符串比较
- `=`:检查两个字符串是否相等。
- `!=`:检查两个字符串是否不相等。
- `-z`:检查字符串是否**为空**(长度为零)。
- `-n`:检查字符串是否**非空**(长度非零)。
- `>`:检查一个字符串是否在字典序上大于另一个字符串()
- `<`:检查一个字符串是否在字典序上小于另一个字符串。
注意:
- **`>` 和 `<` 操作符==必须转义==,或者放在双括号 `[[]]` 中进行**,否则会被shell解释为重定向符号,将字符串值当作文件名
- `>` 和 `<` 使用**标准的 Unicode 顺序**,根据每个字符的 Unicode 编码值来决定排序结果
- `sort` 命令使用的是系统的语言环境设置中定义的排序顺序。
示例一:
```shell
str1="Hello"
str2="World"
# 检查 $str1 和 $str2 是否相等或不相等
if [ "$str1" = "$str2" ] then;
echo "str1 equals str2"
elif [ "$str1" = "$str2" ] then;
echo "str1 dose not equal str2"
fi
# 检查 $str1 是否为空或非空
if [ -z "$str1" ]; then
echo "str1 is empty"
elif [ -n "$str1" ]; then
echo "str1 is not empty"
fi
```
示例二:使用 `<` 和 `>` 进行字符串比较
```shell
str1="apple"
str2="banana"
if [[ "$str1" > "$str2" ]]; then
echo "$str1 is greater than $str2 in lexical order"
elif [[ "$str1" < "$str2" ]]; then
echo "$str1 is less than $str2 in lexical order"
fi
```
### 文件比较
使用特定的测试运算符来检查文件、目录的各种属性或两个文件之间的关系.
##### 检查文件存在性
- `-e`:检查文件是否存在
- `-s`:检查文件是否**存在且非空**(文件大小大于零)。
##### 检查文件类型
- `-f`:检查是否为普通文件。
- `-d`:检查是否为目录。
- `-L`:检查是否为符号链接。
##### 检查文件权限
- `-r`:检查文件是否可读。
- `-w`:检查文件是否可写。
- `-x`:检查文件是否可执行。
##### 检查文件所属
- `-O`:检查文件是否存在且**属于当前用户所有**
- `-G`:检查文件是否存在且**默认组与当前用户相同**
##### 比较文件日期时间(modification date,最后修改时间)
- `file1 -nt file2`:检查 file1 是否比 file2 更新(newer than)
- `file1 -ot file2`:检查 file2 是否比 file2 更旧(odder than)
示例:
```shell
file1="sample1.txt"
file2="sample2.txt"
# 检查是否为普通文件
if [ -f "$file1" ]; then
echo "$file1 exists and is a regular file"
fi
# 比较两文件日期
if [ "$file1" -nt "$file2" ]; then
echo "$file1 is newer than $file2."
fi
# 检查是否为目录
if [ -d "$file1" ]; then
echo "$file1 is a directory"
fi
# 检查文件是否可读且可写
if [ -r "$file1" ] && [ -w "$file1" ]; then
echo "$file1 is readable and writable"
fi
```
### 逻辑运算符
**逻辑与(AND)**,有两种:
- `-a`(在 `[ ]` 中使用
- `&&`(在 `[[ ]]` 或单独条件语句中使用)
```shell
# `-a`在`[]`中使用
if [ "$a" -gt 10 -a "$b" -gt 20 ]; then
echo "Both conditions are true."
fi
# `&&`在`[[ ]]`中使用
if [[ "$a" -gt 10 && "$b" -gt 20 ]]; then
echo "Both conditions are true."
fi
# `&&`连接单独的条件语句
if [ "$a" -gt 10 ] && [ "$b" -gt 20 ]; then
echo "Both conditions are true."
fi
```
**逻辑或(OR)**,有两种:
- `-o`(在 `[ ]` 中使用)
- `||`(在 `[[ ]]` 或单独条件语句中使用)
```shell
# `-o`在`[]`中使用
if [ "$a" -gt 10 -o "$b" -gt 20 ]; then
echo "At least one condition is true."
fi
# `||`在`[[ ]]`中使用
if [[ "$a" -gt 10 || "$b" -gt 20 ]]; then
echo "At least one condition is true."
fi
# `||`连接单独的条件语句
if [ "$a" -gt 10 ] || [ "$b" -gt 20 ]; then
echo "At least one condition is true."
fi
```
### 单括号命令
单括号 `()` 允许在 if 语句中使用子 shell,形式为:`(command)`
shell将会创建一个子shell,在子shell中执行单括号内的命令。
```shell
#!/bin/bash
# Testing a single parentheses condition
echo $BASH_SUBSHELL # 在主shell中执行, 未使用子shell, 输出0
if (echo $BASH_SUBSHELL); then # 在子shell中执行, 输出1
echo "The subshell command operated successfully."
else
echo "The subshell command was NOT successful"
fi
```
### 双括号命令
双括号命令`[[ ]]`,支持在比较条件中使用高级表达式。
> 
<br><br>
## case语句
case语句:
```bash
case expression in
pattern1)
commands1;;
pattern2)
commands2;;
pattern3 | pattern4)
commands3;;
*)
default_commands;;
esac
```
- `expression`:这是你要测试的值或变量。
- `pattern1`, `pattern2`, ...:这些是要匹配的模式。Shell将会测试 `expression` 是否符合这些模式。
- `commands1`, `commands2`, ...:当相应的模式匹配时,这些命令将会被执行。
- `*`:这是一个通配符,用于匹配任何不符合上述所有模式的情况。
- `;;`:表示一个**模式的命令列表结束**。(类似于c++中的`break`)
- `esac`:`case` 的反向拼写,表示 `case` 语句的结束。
示例:
```shell
#!/bin/bash
case $USER in
rich | christine)
echo "Welcome $USER"
echo "Please enjoy your visit." ;;
barbara | tim)
echo "Hi there, $USER"
echo "We're glad you could join us." ;;
testing)
echo "Please log out when done with test." ;;
*)
echo "Sorry, you are not allowed here." ;;
esca
```
<br><br>
## 循环命令
### for 语句
```shell
for var in list
do
commands
done
# 或者
for var in list; do
commands
done
```
在最后一次迭代之后,`var` 变量的值**在 shell 脚本中的剩余部分仍然有效**,会一直保持最后一次迭代时候的值,除非做了修改。(这里相当于在脚本内定义了一个全局变量 `var`)
> [!caution]
>
> - 如果**列表值**中某一项包含**单引号** `'` ,必须在单引号前使用转义符`\`,或者将这一项放入双引号`"" ` 内,否则会解析错误。
>
> ```shell
> for test in I don\'t know if "this'll" work
> do
> echo "word:${test}"
> done
> ```
>
> - 如果列表值中某一项包含**空格** ` `,必须将该项放入双引号 `""` 内。
>
> ```shell
> for test in Nevada "New Hampshire" "New Mexico" "New York"
> do
> echo "Now going to $test"
> done
> ```
>
>
#### for 语句使用示例
使用示例一:遍历**值列表**
```shell
for test in Alabama Alaska Arizona Arkansas California Colorado
do
echo "The next state is $test"
done
```
使用示例二:从**变量**中读取**值列表**
```shell
list="Alabama Alaska Arizona Arkansas Colorado"
list=$list" Connecticut"
for state in $list
do
echo "Have you ever visited $state?"
done
```
使用示例三:从**命令**中读取**值列表**
通过 "命令替换" 来执行一个能产生输出的命令,并在 `for` 命令中**使用该命令的输出**。
```shell
#!/bin/bash
# reading values from a file
file="states.txt"
IFS=\n'
for state in $(cat $file)
do
echo "Visit beautiful $state"
done
```
上例中, `IFS` 环境变量定义了 `bash shell` 用作字段分隔符的一系列字符,默认包括:**空格、制表符、换行符**。
通过在 shell 脚本中临时更改 `IFS` 环境变量,可以使其只识别特定换行符:
使用示例四:**遍历目录中的文件**
```shell
#!/bin/bash
# iterate through all the files in a directory
# `for`命令会遍历`home/rich/test/*`匹配的结果
for file in /home/rich/test/*
do
if [ -d "$file" ]; then
echo "$file is a directory"
elif [ -f "$file" ]; then
echo "$file is a file"
fi
done
# `for` 可以匹配多个通配符
for file in /home/rich/.b* /home/rich/badtest
do
if [ -d "$file" ]; then
echo "$file is a directory"
elif [ -f "$file" ]; then
echo "$file is a file"
else
echo "$file doesn't exist"
fi
done
```
使用示例五:**遍历位置参数**
```shell
#!/bin/bash
for arg in "$@"
do
echo "Argument: $arg"
done
```
#### C风格的 for 循环
> [!caution]
> **==C 风格的 for 循环是 Bash 特有的==**!**不是 POSIX shell 的一部分**。
> 它可能不会在其他 shell 中工作,比如 Korn shell (ksh) 或传统 Bourne shell (sh)。
**bash** 中 **仿 C 语言的 for 循环**的基本格式如下:
`for (( variable assignment ; condition ; iteration process )) `
示例:`for (( a = 1; a < 10; a++ ))`
与 bash shell 标准的 for 命令存在的差异为:
- 变量赋值时**可以有空格**;
- 迭代条件中使用变量值时**无需用 `
或 `${}` 引用**;
- 迭代过程的算式**不需要使用 `expr` 命令的格式**;
- 可以使用多个变量进行迭代,但只能**定义一种迭代条件**
使用示例一:
```shell
#!/bin/bash
# testing the C-style for loop
for (( i = 1; i <= 10; i++ ))
do
echo "The next number is $i"
done
```
使用示例二:
```shell
#!/bin/bash
# multiple variables
for (( a=1, b=10; a <= 10; a++, b-- ))
do
echo "$a - $b"
done
```
### while 语句
与 `if` 语句一样,while 语句后**默认只能跟 "命令"**,根据命令的退出状态码来进行判断,但可以使用 `test` 命令或单括号 `[]` 来进行**条件测试**,例如测试变量值。
while 语句中可以使用**多个"测试命令"**,将**只根据最后一个命令的退出状态码来进行判断**。
其中,**每个测试命令需要单独放在一行中**。
基本语法:
```shell
while test_command
do
other_commands
done
# 或者
while test_command; do
other commands
done
```
> [!caution]
> `test_command` 的退出状态码**必须随着循环中执行的命令而改变**,否则意味着成为死循环。
使用示例:
```shell
#!/bin/bash
# while command test
var1=10
while [ $var1 -gt 0 ]
do
echo $var1
var1=$[ $var1 - 1 ]
done
```
使用示例二:多个测试命令,每个测试命令需要单独放在一行中
```shell
#!/bin/bash
# multi test commands
var1=10
while echo $var1
[ $var1 -ge 0 ]
do
echo "This is inside the loop"
done
# 上述命令最后三行输出如下:
# 0
# This is inside the loop
# -1
```
### until 语句
until 命令中,当测试命令返回 "非0" 退出状态码时会持续循环,**直到测试命令返回退出状态码 0** 。
与 while 语句类似,until 中可以使用**多个"测试命令"**,将**只根据最后一个命令的退出状态码来进行判断**。其中,每个测试命令需要单独放在一行中。
基本格式:
```shell
until test_command
do
other_commands
done
```
使用示例一:
```shell
#!/bin/bash
# using the until command
var1=100
until [ $var1 -eg 0 ]
do
echo $var1
var1=$[ $var1 - 25 ]
done
```
使用示例二:
```shell
#!/bin/bash
# using the until command
var1=100
until echo $var1
[ $var1 -eq 0 ]
do
echo Inside the loop: $var1
var1=$[ $var1 - 25 ]
done
```
### 嵌套循环
使用示例一:
```shell
#!/bin/bash
# nesting for loops
for (( a=1; a<=3; a++ ))
do
echo "Staring loop $a"
for (( b=1; b<=3; b++ ))
do
echo " Inside loop: $b"
done
done
```
使用示例二:
```shell
#!/bin/bash
# placing a for loop inside a while loop
var1=5
while [ $var1 -ge 0 ]
do
echo "Outer loop: $var1"
for (( var2=1; var2<3; var2++ ))
do
var3=$[ $var1 * $var2 ]
echo " Inner loop: $var1 * $var2 = $var3"
done
var1=$[ $var1 - 1 ]
done
```
### 循环控制
- break 命令
- `break` "跳出"当前层循环
- `break n` n指定跳出循环的层级,默认为1即当前层;
- continue 命令
- `coninue` "跳过"当前层的本轮循环
- `continue n` n指定跳过第n层的本轮循环,默认为1,即跳过当前层的本轮循环。
### 传递/重定向循环输出
- 将循环输出重定向到文件
```shell
#!/bin/bash
# redirecting the for output to a file
for (( a=1; a < 10; ++a)); do
echo "The number is $a"
done > for_redirect.txt
echo "The command is finished."
```
<br><br><br>
# 命令行选项与参数处理
## 命令行参数
"**==命令行参数==**" 允许运行脚本时在命令行中传递参数给脚本。
bash shell 会将所有命令行参数指派给称为 "**==位置参数==**" 的**特殊变量**,包括shell脚本名称。
位置变量的名称都是**标准数字**:
- `$0`:**脚本名字**(取决于运行脚本时的方式和路径)
- `$1` ~ `$9` :第一个到第九个命令行参数
- `${10}` ~... : 第十个及以上的命令行参数,引用这些变量值时**必须使用`${}`,带上花括号**。
命令行参数可以是**数值、字符串**(如果包含空格则必须加上引号,单双引号均可)
`$0` 位置参数示例:
| | 运行命令 |
| -------------------------- | -------------------------- |
| `$ bash test.sh` | test. sh |
| `$ ./test.sh` | ./test. sh |
| `$ $HOME/scriptes/test.sh` | /home/yht/scripts/test. sh |
<br>
## 移动命令行参数
命令:`shift [n]` :一次性左移 `n` 个位置(省略 `n` 时默认为 1)。
`shift` 命令默认会将**每个位置的变量值都向左移动一个位置**。
因此,变量 `$3` 的值会移入 `$2`,变量 `$2` 的值会移入 `$1`,而**变量 `$1` 的值则会被删除**。
(变量 `$0` 的值,也就是脚本名,不会改变)。
位置参数变量**被移出后就无法恢复**。
<br>
## 处理命令行参数与选项
**==命令行选项==** 是带有连字符的、特殊的命令行参数,因此可以按处理普通命令行参数的方式进行识别。
#### 只包含选项时
```shell
#!/bin/bash
echo
while [ -n "$1" ]; do
case "$1" in
-a) echo "Found the -a option";;
-b) echo "Found the -b option";;
-c) echo "Found the -c option";;
*) echo "$1 is not an option";;
esac
shift # 注意必须shift!
done
```
#### 同时包含选项和参数时
##### 方式一:使用双连字符 `--` 分隔选项和参数
在Linux中,**使用双连字符 `--` 来标识选项部分的结束**,因此可**在脚本中**通过对位置参数识别 `--` 来进行判断。
但是,这 **要求运行脚本时必须显式地以 `--` 来==分隔选项和参数==** ,例如:
````shell
./test.sh -a -b -c -- param1 param2 param3`
````
脚本内识别到 `--` 时则跳出循环,结束选项的处理,开始处理参数:
```shell
#!/bin/bash
# Extract command-line options and parameters
# 运行示例: `./test.sh -a -b -c -- param1 param2 param3`
echo
while [ -n "$1" ]; do
case "$1" in
-a) echo "Found the -a option" ;;
-b) echo "Found the -b option" ;;
-c) echo "Found the -c option" ;;
--) shift
break;;
*) echo "Invalid option: $1" >&2
exit 1 ;;
esac
shift
done
count=1
for param in "$@"; do
echo "Parameter #$count: $param"
((++count))
done
exit
```
##### 方式二:使用 `getopts` 或更高级的`getopt` 命令
略。
### 使用内置 `getopts` 命令解析选项和参数
bash提供了**内置的 `getopts` 命令专门用于处理短选项**,如果要处理长选项需要使用外部命令 `getopt`。
`getopts` 命令格式:`getopts optstring variable`
- `optstring` 中列出有效的选项字母(因此**只支持单字符的短选项**)
- 如果选项 **==要求有参数值==**就在其后加一个冒号
- 如果 **==不想显示错误消息==**,就在 `optsring` 前加一个冒号。
`getopts` 命令会用到两个环境变量:
- `OPTARG`:保存当前**选项附带的参数值**
- `OPTIND`:保存着参数列表中 getopts **正在处理的参数位置**,用于跟踪 getopts 已经处理到哪个命令行参数,默认初始值是1,即指向命令行的第一个参数。
- `getopts` 每处理一个选项时,`OPTIND` 的值会递增,以指向下一个要处理的参数的位置。
**getopts 每次只从命令行参数中处理一个检测到的命令行选项,将选项名赋给变量`variable`,同时会将环境变量`OPTIND` 值增加1,指向后一个命令行参数的位置。** **在处理完所有选项后,getopts 会退出并返回一个大于 0 的退出状态码**。
getopt 会将从命令行参数中**检测到的所有未定义的选项统一输出为问号 `?`
(即赋给变量的是 `?`, 此时无效的选项本身会被存储在变量 `OPTARG` 中)**
示例:
```shell
#!/bin/bash
# 使用`getopts`命令处理短选项-a, -c, -v
# 运行示例:
# `./process_param_option_getopts.sh -a -c -v 25 -d "Bulo bole" O3 p2 b88 V99`
# `./process_param_option_getopts.sh -acv25 -e`
while getopts ":av:cd:" opt; do
case $opt in
a) echo "Found the -a option" ;;
c) echo "Found the -c option" ;;
v) echo "Found the -v option, with parameter value $OPTARG" ;;
d) echo "Found the -d option, with parameter value $OPTARG" ;;
\?) echo "Invalid option: -$OPTARG" >&2
exit 1 ;;
esac
done
# 移动参数列表中的参数指针, 将其移动到第一个非选项参数的位置
shift $((OPTIND - 1))
# 处理非选项参数
while [ -n "$1" ]; do
echo "Found the parameter: $1"
shift
done
exit
```
<br><br><br>
# 读取用户输入: `read` 命令
`read` 命令,从**标准输入(键盘)** 或 **一个文件描述符** 中接受输入。
命令:`read variable`
- 获取输入后,`read` 命令会将**数据存入变量 `variable`**。
- 未指定变量时,所有收到的数据将**存放入环境变量`REPLY`中**。
`read` 命令选项:
- `-r` :不转义任何字符,禁止反斜杠`\`转义任何字符。
- `-p`:指定输入提示符,例如`read -p "Please enter your age: " age`
- `-t <num>` :指定等待输入的秒数,计时器超时后 `read` 将返回非0退出状态码。
- `-n <num>`:在接收到指定个数的字符后退出,将已输入的数据赋给变量。
- `-s`:隐藏输入的内容,使屏幕上不可见
- **实际上数据还是会被显示,只是 `read` 命令将文本颜色设成了跟背景色一样。**
read命令还可用于读取文件,每次调用 `read` 命令都会从指定文件中读取一行文本。当文件中没有内容可读时,read 命令会退出并返回非0退出状态码。
示例:
```shell
#!/bin/bash
# Using the read command to read a file
count = 1
cat $HOME/scripts/test.txt | while read line
do
echo "Line $count: $line"
done
echo "Finished processing the file."
exit
```
<br><br><br>
# 捕获系统信号: `trap` 命令
`trap` 命令用于捕获Linux系统信号或其他特定的系统事件,可用在shell脚本中**指定当接收到特定信号时执行的命令或脚本段**。当脚本收到了指定信号时,该信号不再由shell处理,而是由脚本自行处理。
命令格式:`trap commands signals`
- `commands`:当捕获到指定的信号或事件时,要执行的命令或脚本代码。
- `signals`:要捕获的信号(如`SIGINT`、`SIGTERM`) 或系统事件,可使用信号名或信号值。
使用说明:
- **重置 `trap`**: `trap - signals`,取消 `trap` 之前对该信号的处理,**恢复信号的默认行为**。
- 如果在脚本中的不同位置进行不同的信号捕获处理,只需**重新使用带有新选项的` trap` 命令即可**。
- **完全忽略信号**:`trap "" SIGINT`
- 在shell脚本**退出时捕获 `EXIT` 信号**:`trap commands EXIT`
捕获"信号"示例一: 捕获`SIGINT(2)`, 即`Ctrl+C`
```shell
#!/bin/bash
# Testing signal trapping
# 该命令开启了一个循环, 每次sleep 1s, 捕获了SIGINT信号, 因此Ctrl+C无法将其终止.
trap "echo ' Sorry! I have trapped Ctrl-C'" SIGINT
echo This is a test script.
count=1
while [ $count -le 7 ]; do
echo "Loop #$count"
sleep 1
((++count))
done
echo "This is the end of test script."
exit
```
捕获"系统事件"示例:捕获`EXIT` 信号,退出脚本时清理临时文件
```shell
# 在脚本退出时(正常退出或因接收到信号而退出时), 删除一个临时文件.
trap "rm -f $tempfile; exit" EXIT
```
示例:捕获信号 `SIGHUP(1)`、`SIGINT(2)`和 `SIGTSTP(20)`,进行处理。
<br><br><br>
# Shell 函数
bash shell 提供的 "函数" 本质是一种 "**小型脚本**",表现为**将"多个命令"封装为一个命令**。
特点为:
- shell 函数**支持 "传入参数"**,但**没有形参列表**,直接在函数体内**使用位置变量**。
- shell 函数**没有返回类型**,其**运行结束时返回一个"退出状态码"(0~255)**。
- 调用 shell 函数**直接使用函数名**,不带圆括号 `()`,**传递参数如同 "向脚本传递命令行参数"**,必须将参数和函数名放在同一行。
> [!caution]
> 由于函数使用**位置变量**来访问函数参数,因此**函数内无法直接获取脚本的命令行参数**。
> 要在函数中使用脚本的命令行参数,必须**调用函数时手动传入**。
>
### 定义及调用 shell 函数
两种形式声明函数:
```shell
# 形式一: 显式使用`function`关键字
function MyFuncName {
echo "Hello, world"
}
# 形式二: 在函数名后使用圆括号`()`
MyFunctionName() {
echo "Hello, world"
}
```
函数参数:
```shell title:func_param.sh
# 函数参数: 没有形参列表, 直接在函数体内使用`$1`, `$2`, `$3`...等位置参数
MyFunc3() {
echo "This is MyFunc3"
echo "It accept a parameter: $1"
echo "It also accept a parameter: $2"
}
# 直接以"传递脚本参数"的形式传递给函数参数.
MyFunc3 "Hello" "Shell"
```
### 函数内定义局部变量
在 shell 脚本中直接定义的任何变量**默认为==全局变量==**,在整个脚本内可见。
在 shell 函数中,可通过在**变量名前加上 `local`** 来 **定义==只在函数体内有效==的==局部变量==**。
### 向函数传递数组
向 shell 函数传递数组由如下几种方式:
1. 使用数组元素
2. 通过全局数组
3. 通过间接引用(since Bash 4.3)
<br><br>
# 参考资料
# Footnotes
[^1]: [Shebang (Unix) - Wikipedia](https://en.wikipedia.org/wiki/Shebang_(Unix))
[^2]: [Shell Tools and Scripting · Missing Semester](https://missing.csail.mit.edu/2020/shell-tools/)
[^3]: 《UNIX 环境高级编程》(P208)