%% # 缓冲区笔记 本文件主要记录 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] 方式一 > ![image-20230918223127260|894](_attachment/02-开发笔记/11-Linux/shell%20相关/shell%20脚本.assets/IMG-shell%20脚本-897A96C3E07F30D3DDEC794CBF635828.png) > [!note] 方式二 > ![image-20230918223333328|889](_attachment/02-开发笔记/11-Linux/shell%20相关/shell%20脚本.assets/IMG-shell%20脚本-792A0928551BBDA38859BB8C22BD2A06.png) <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 ``` ### 双括号命令 双括号命令`[[ ]]`,支持在比较条件中使用高级表达式。 > ![image-20231231112937561|730](_attachment/02-开发笔记/11-Linux/shell%20相关/shell%20脚本.assets/IMG-shell%20脚本-DD30473E46EE58E7514572E98589BCB3.png) <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)