《Linux Shell 编程基础精要:语法规则、变量解析与注释技巧》
1. Linux Shell 脚本基础语法
1.1. 脚本开头
Shell 脚本的开篇首行,通常为 Shebang 行,其核心作用是精准指定脚本的解释器。在众多场景中,Bash 解释器应用极为广泛,例如:
#!/bin/bash
这行代码如同给系统下达的明确指令,告知系统使用/bin/bash来对后续脚本内容进行解析与执行。
需特别注意,Shebang 行必须严格置于脚本的第一行位置,否则,脚本在执行过程中可能会遭遇各种异常,导致无法正常运行。在某些复杂的系统环境中,若同时安装了多个不同版本的 Bash,通过这种指定完整路径的方式,就能够确保脚本被特定版本的解释器正确处理,有效避免因解释器版本差异引发的兼容性问题。
1.2. 执行权限
若要让一个 Shell 脚本顺利运行起来,赋予其执行权限是必不可少的关键步骤,而实现这一操作,我们依赖chmod命令,具体示例如下:
chmod +x script.sh
这里的script.sh代表你实际编写的脚本文件名。chmod命令主要用于灵活调整文件或目录的访问权限,而+x这一参数选项,明确表示为目标文件添加可执行权限。只有当脚本被成功赋予可执行权限后,系统才会将其识别为可执行程序,进而顺利运行。倘若在未添加执行权限的情况下贸然尝试运行脚本,系统会即刻反馈权限不足的错误提示,阻止脚本的执行。
1.3. 执行脚本
执行脚本的常见方式如下:
./script.sh
这里的./明确表示当前目录。在 Linux 系统的运行机制中,当你发起执行一个脚本的指令时,如果该脚本所处位置不在系统PATH环境变量所涵盖的目录范围之内,那么就必须通过相对路径(如./)或者绝对路径的方式,精准指定脚本的具体位置,系统才能准确找到并执行它。反之,如果脚本恰好位于PATH环境变量所包含的目录中,那么直接使用脚本名即可完成执行操作,这极大地提高了操作的便捷性。
2. 注释
在 Shell 脚本的编写过程中,注释发挥着至关重要的作用。注释均以#符号作为起始标识,其主要功能在于对代码的功能、用途进行详细阐释,或者添加各类有助于理解的说明信息。值得注意的是,注释部分的内容不会被解释器纳入执行范畴。
2.1. 单行注释
单行注释的使用方式极为简单,只需在一行代码之前添加#符号即可,示例如下:
# 这是一个单行注释
echo "Hello, World!"
单行注释常用于针对某一行具体代码,给出简洁明了的解释说明,方便脚本编写者自身日后回顾,同时也便于其他人员阅读和理解代码的实际作用,提升代码的可读性。
2.2. 多行注释
尽管 Shell 本身并未内置专门的多行注释语法,但我们可以巧妙借助未使用的函数结构,来模拟实现多行注释的效果,示例如下:
: '
这是一个多行注释示例
可以在这里详细阐述脚本的设计思路、功能细节、实现逻辑以及使用过程中的注意事项等多方面信息。
'
echo "This is a script."
多行注释通常应用于对一段相对复杂的代码块,或者整个脚本的核心功能进行全面、深入的说明。通过多行注释,能够详细描述脚本的功能特性、使用方法、潜在风险以及注意事项等关键内容,为脚本的维护和优化提供有力支持。
3. 变量
3.1. 用户自定义变量
用户自定义变量是由用户依据实际需求自主定义并使用的变量类型,主要用于存储各种特定的数据信息。在命名规则方面,变量名仅允许包含字母、数字以及下划线,并且严格禁止以数字作为开头。
- 定义和赋值:采用变量名=值的简洁形式进行定义和赋值操作,特别要注意的是,等号两边不允许出现空格。示例如下:
在定义变量时,其值可以是多样化的数据类型,如常见的字符串、数字,以及文件路径等。当值中包含空格字符时,为确保数据的完整性和准确性,需要使用引号(单引号或双引号均可)将其括起来。其中,双引号具备变量解析功能,会对其中的变量进行替换解析;而单引号则会严格保持变量的原始状态,原样输出。
- 引用变量:在引用变量的值时,只需在变量名前添加$符号即可。若要在字符串中引用变量,强烈建议使用{}将变量名括起来,以此有效避免可能出现的解析错误。例如:
3.2. 位置参数变量
位置参数在脚本编写中扮演着重要角色,主要用于在脚本运行过程中传递外部参数。通过$1、$2、$3…… 这样的形式,分别对应表示传递给脚本的第一个、第二个、第三个参数,依此类推。特别地,$0被系统约定用来表示脚本本身的名称。以下是一个示例脚本test.sh:执行脚本并传递参数的操作如下:
位置参数在编写需要接收外部输入的脚本时,具有极高的实用价值。例如,当编写一个用于批量处理文件的脚本时,待处理的文件名就可以通过位置参数便捷地传递给脚本,实现灵活高效的文件处理操作。
3.3. 预定义变量
预定义变量是由 Shell 系统预先定义好的特殊变量,每一个都被赋予了特定的含义和用途。
- $#:用于表示传递给脚本的参数个数,通过它可以快速知晓外部传入参数的数量信息。
- $*:将所有传递给脚本的参数视为一个整体,方便进行统一处理。
- $@:同样表示所有传递给脚本的参数,但与$*不同的是,$@会将每个参数作为独立的个体进行处理,在某些场景下能够提供更精细的操作。
- $?:用于反馈上一个命令的退出状态,其中 0 代表命令执行成功,非 0 则表示命令执行过程中出现了错误或异常情况。
- $$:表示当前脚本运行时的进程 ID,通过这个变量可以对脚本进程进行特定的管理和操作。
以下是一个展示预定义变量使用的示例脚本test.sh:
在实际脚本编写中,$?变量的应用十分广泛,常用于判断上一个命令是否成功执行,进而依据执行结果在脚本中执行不同的操作逻辑。例如:
3.4. 环境变量
环境变量属于全局变量的范畴,其作用范围涵盖所有子进程,并且能够对系统以及各类应用程序的运行行为产生影响。
- 查看环境变量:可以使用env或者printenv命令来查看系统中所有的环境变量信息;若要查看单个环境变量的值,使用echo $变量名即可。例如:
其中,PATH环境变量尤为重要,它包含了一系列的目录路径信息。当在命令行中执行一个命令时,系统会按照PATH环境变量中所包含的目录顺序,依次查找对应的可执行文件,从而确保命令能够正确执行。
- 设置环境变量:使用export命令可以将普通变量提升为环境变量,或者在定义变量时直接使用export进行定义。例如:
# 定义一个普通变量
my_var="hello"
# 将其提升为环境变量,使其具备全局作用域
export my_var
# 或者直接定义为环境变量
export new_var="world"
一旦设置了环境变量,在当前 Shell 会话及其衍生的所有子进程中,都可以对该环境变量进行访问和使用。若希望环境变量在每次用户登录系统时都能自动生效,可以将其添加到~/.bashrc或者~/.bash_profile文件中,实现环境变量的持久化配置。
- 修改环境变量:修改环境变量时,可以直接对其进行重新赋值操作,并使用export命令使其生效。例如:
# 修改 PATH 环境变量,添加一个新的目录,扩展系统搜索路径
export PATH=$PATH:/new/directory/path
在修改PATH环境变量时,务必注意不要错误地覆盖原有的值,而是应该将新的目录路径追加到原有的PATH变量值之后,以确保系统原有的搜索路径功能不受影响。
4. Linux Shell 程序和语句
在 Linux Shell 编程中,程序和语句可以分为三种主要类型:类型语句、功能性语句和结构性语句。以下是这些类型的详细解析。
4.1. 类型语句(Type Statements)
类型语句用于定义和操作数据类型。虽然 Shell 是一种弱类型语言,但在某些情况下,了解变量的类型仍然是有帮助的。
4.1.1. 变量声明
Shell 中的变量不需要显式声明类型,默认为字符串类型。可以通过赋值来定义变量。
MY_VAR="Hello" NUM=42
4.1.2. 数字类型
虽然 Shell 不区分整数和浮点数,但可以使用 bc
或 awk
进行浮点运算。
-
整数运算:
SUM=$((2 + 3)) echo $SUM
-
浮点运算:
FLOAT_SUM=$(echo "scale=2; 2.5 + 3.7" | bc) echo $FLOAT_SUM
4.1.3. 字符串类型
字符串是默认的数据类型,支持单引号和双引号。
-
单引号:不进行变量替换或转义。
-
双引号:支持变量替换和转义字符。
-
示例:
#!/bin/bash
# 定义一个字符串变量 STR1,使用单引号,其中的内容不会进行变量替换
STR1='Hello, World!'
# 定义一个字符串变量 STR2,使用双引号,其中的内容会进行变量替换
# $USER 是一个环境变量,表示当前登录的用户名
STR2="Hello, $USER!"
# 输出变量 STR1 的内容
echo $STR1
# 输出变量 STR2 的内容
echo $STR2
4.1.4. 数组类型
Shell 支持一维数组,可以通过索引访问元素。
-
定义数组:
ARRAY=("apple" "banana" "cherry")
# 目的:定义一个名为ARRAY的数组,用于存储水果名称
# 数组:在Shell脚本中,数组是一种特殊的变量,它可以同时存储多个值
# 元素:数组中的每个值被称为一个元素,通过索引可以访问这些元素
# 此处,我们创建了一个包含三个元素的数组,元素分别为 "apple"、"banana" 和 "cherry"
# 注意:在Bash中,数组的索引从0开始,即第一个元素的索引是0,第二个是1,以此类推
- 访问数组元素:
echo ${ARRAY[0]} # 输出: apple echo ${ARRAY[@]} # 输出所有元素
# 访问数组的第一个元素
echo ${ARRAY[0]} # 输出: apple
# 访问数组的第二个元素
echo ${ARRAY[1]} # 输出: banana
# 访问数组的第三个元素
echo ${ARRAY[2]} # 输出: cherry
# 输出数组中的所有元素
echo ${ARRAY[@]} # 输出: apple banana cherry
- 获取数组长度
# 输出数组的长度(元素个数)
echo ${#ARRAY[@]} # 输出: 3
4.1.5. 数组遍历
# 循环遍历数组中的每个元素
for fruit in "${ARRAY[@]}"; do
echo "Fruit: $fruit"
done
4.1.6 关联数组
关联数组允许使用字符串作为键。
-
定义关联数组:
declare -A ASSOC_ARRAY ASSOC_ARRAY=(["key1"]="value1" ["key2"]="value2")
-
访问关联数组元素:
echo ${ASSOC_ARRAY["key1"]}
# 输出: value1
4.2. 功能性语句(Functional Statements)
功能性语句用于执行特定功能或操作,如条件判断、循环、函数等。
4.2.1. 条件语句
条件语句用于根据条件执行不同的代码块。
-
if 语句:
if [ condition ]; then
# 执行代码
elif [ another_condition ]; then
# 执行代码 else # 执行代码 fi
-
示例:
# 检查变量 NUM 的值是否大于 10
# if 是条件判断的起始关键字,用于开启一个条件判断结构
# [ ] 是测试命令,用于进行条件测试
# "$NUM" 是要进行比较的变量,使用双引号防止变量为空时出现错误
# -gt 是比较运算符,表示大于
# then 表示如果条件为真,则执行接下来的代码块
if [ "$NUM" -gt 10 ]; then
# 如果变量 NUM 的值大于 10,使用 echo 命令输出提示信息
echo "Number is greater than 10."
# elif 用于在前面的 if 条件不成立时,继续检查新的条件
# 这里检查变量 NUM 的值是否等于 10
# -eq 是比较运算符,表示等于
elif [ "$NUM" -eq 10 ]; then
# 如果变量 NUM 的值等于 10,使用 echo 命令输出提示信息
echo "Number is equal to 10."
# else 表示如果前面的 if 和 elif 条件都不成立,则执行这里的代码块
else
# 输出提示信息,表示变量 NUM 的值小于 10
echo "Number is less than 10."
# fi 是条件判断语句的结束关键字,标志着整个 if-elif-else 条件判断结构的结束
fi
-
case 语句:
case $VAR in pattern1)
# 执行代码 ;; pattern2)
# 执行代码 ;; *)
# 默认情况 ;; esac
-
示例:
#!/bin/bash
# 定义一个变量 OPTION,假设其值从命令行参数获取
OPTION=$1
# 使用 case 语句根据 OPTION 的值执行不同的操作
case $OPTION in
start)
# 如果 OPTION 的值是 "start",则输出 "Starting service..."
echo "Starting service..."
;;
stop)
# 如果 OPTION 的值是 "stop",则输出 "Stopping service..."
echo "Stopping service..."
;;
restart)
# 如果 OPTION 的值是 "restart",则输出 "Restarting service..."
echo "Restarting service..."
;;
*)
# 如果 OPTION 的值不是上述任何一种情况,则输出 "Invalid option."
echo "Invalid option."
;;
esac
4.2.2. 循环语句
循环语句用于重复执行代码块。
-
for 循环:
# for循环基础语法结构
# for 循环用于遍历一个列表中的每个元素,每次循环将列表中的一个元素赋值给指定的变量,然后执行相应的命令序列
# 以下是详细的语法结构解释
# "for" 是循环的关键字,标志着 for 循环的开始
# "变量名" 是自定义的一个变量,在每次循环中,列表中的元素会依次赋值给这个变量
# "in" 是一个分隔关键字,用于分隔变量名和要遍历的列表
# "列表" 是由空格分隔的一组元素,可以是数字、字符串等,循环会依次取出列表中的每个元素
# "do" 是循环体开始的标志,表明从这里开始是要执行的命令序列
# "命令序列" 是在每次循环中要执行的一系列命令,可以是单个命令,也可以是多个命令的组合
# "done" 是循环结束的标志,当列表中的所有元素都被遍历完后,循环结束
for 变量名 in 列表
do
命令序列
done
-
示例:
-
while 循环:
# while 循环是 Shell 脚本中用于实现条件循环的结构,只要条件测试为真,就会不断执行循环体中的命令序列
# 以下是对 while 循环各部分的详细解释
# "while" 是关键字,用于开始一个 while 循环结构
# "条件测试" 是一个表达式,用于判断是否满足进入循环体的条件。
# 该表达式通常使用 Shell 的测试命令(如 [ ] 或 [[ ]]),或者执行其他命令,根据其退出状态码来判断真假
# 退出状态码为 0 表示条件为真,非 0 表示条件为假
while 条件测试
do
# "do" 是关键字,标志着循环体的开始
# 从这里开始到 "done" 之间的所有命令构成了循环体
# "命令序列" 是在每次循环中要执行的一系列命令
# 这些命令可以是任何有效的 Shell 命令,用于完成特定的任务
命令序列
# "done" 是关键字,标志着 while 循环体的结束
# 当条件测试的结果为假时,循环结束,程序继续执行 "done" 之后的代码
done
-
示例:
#!/bin/bash
# 指定脚本的解释器为 Bash,意味着该脚本将由 /bin/bash 程序来执行
# 初始化计数器为 1
# 定义一个名为 count 的变量,并将其初始值设置为 1,后续将利用这个变量来控制循环次数
count=1
# 使用 while 循环,只要计数器小于等于 5 就继续循环
# while 是循环关键字,用于开启一个循环结构
# [ $count -le 5 ] 是条件测试部分,使用了 Shell 的测试命令 []
# $count 是引用前面定义的 count 变量,-le 是比较运算符,表示小于等于
# 整体的意思是判断 count 变量的值是否小于等于 5,如果是则条件为真,进入循环体执行命令
while [ $count -le 5 ]
do
# 输出当前计数器的值
# echo 是用于输出信息的命令
# "当前计数: $count" 是要输出的内容,其中 $count 会被替换为变量 count 的实际值
echo "当前计数: $count"
# 计数器的值加 1
# ((count++)) 是 Shell 中的算术扩展语法,用于对变量进行算术运算
# count++ 表示将 count 变量的值增加 1,这样每次循环计数器的值都会递增
((count++))
# done 关键字表示 while 循环结束,当 [ $count -le 5 ] 条件不成立时,循环终止
done
-
示例:
# until 循环是一种条件循环结构,与 while 循环相反,它会持续执行循环体中的命令序列,
# 直到条件测试的结果为真时才停止循环。以下是对其各部分的详细解释:
# "until" 是该循环结构的起始关键字,用于标识这是一个 until 循环。
# "条件测试" 是一个表达式,用于判断是否满足终止循环的条件。
# 通常使用 Shell 的测试命令(如 [ ] 或 [[ ]]),或者执行其他命令,依据其退出状态码来判断真假。
# 在 until 循环里,退出状态码为 0 表示条件为真,非 0 表示条件为假。
# 只要条件测试结果为假,就会不断执行循环体中的命令序列。
until 条件测试
do
# "do" 是关键字,标志着循环体的开始。
# 从这里开始到 "done" 之间的所有命令构成了循环体。
# "命令序列" 是在每次循环中要执行的一系列命令,
# 这些命令可以是任何有效的 Shell 命令,用于完成特定的任务。
命令序列
# "done" 是关键字,标志着 until 循环体的结束。
# 当条件测试的结果为真时,循环结束,程序继续执行 "done" 之后的代码。
done
#!/bin/bash
# 指定该脚本使用 /bin/bash 作为解释器,即让系统用 Bash 来执行此脚本
# 初始化计数器变量为 1
# 定义一个名为 counter 的变量,并将其初始值设置为 1,后续会用它来控制循环的执行次数
counter=1
# 开始 until 循环,只要 counter 小于等于 5 这个条件为假,就执行循环体
# until 是循环关键字,用于开启 until 循环结构
# [ $counter -gt 5 ] 是条件测试部分,使用了 Shell 的测试命令 []
# $counter 是引用前面定义的 counter 变量,-gt 是比较运算符,表示大于
# 整体含义是判断 counter 变量的值是否大于 5,如果不大于(即小于等于 5),条件为假,进入循环体执行命令
until [ $counter -gt 5 ]
do
# 输出当前计数器的值
# echo 是用于输出信息的命令
# "当前计数: $counter" 是要输出的内容,其中 $counter 会被替换为变量 counter 的实际值
echo "当前计数: $counter"
# 计数器的值加 1
# ((counter++)) 是 Shell 中的算术扩展语法,用于对变量进行算术运算
# counter++ 表示将 counter 变量的值增加 1,使得每次循环后计数器的值都会递增
((counter++))
# done 关键字表示 until 循环结束,当 [ $counter -gt 5 ] 条件为真时,循环终止
done
4.2.3. 函数
函数用于封装可重用的代码块。
-
定义函数:
# 以下是定义一个函数的语法结构,函数在Shell脚本中可以将一组命令封装起来,方便复用和维护
# function_name 是函数的名称,遵循变量命名规则,可由字母、数字和下划线组成,且不能以数字开头
# () 表示这是一个函数定义,在Bash中,定义函数时这对括号是必须的,不过括号内通常不写参数,参数传递在调用函数时完成
# { 标志着函数体的开始,函数体包含了函数要执行的具体命令
function_name()
{
# 这里是函数体,需要放置具体要执行的代码
# 这些代码可以是各种Shell命令的组合,用来完成特定的任务,比如文件操作、数据处理等
# 当函数被调用时,会依次执行函数体里的命令
# 执行代码
# } 标志着函数体的结束,意味着函数定义完成
}
-
调用函数:
#!/bin/bash
# 定义一个名为 function_name 的函数
function_name()
{
# 函数体,这里放置要执行的命令
echo "这是 function_name 函数在执行"
}
# 调用 function_name 函数
# 直接使用函数名,无需添加括号(在Bash中,函数调用时括号不是必需的)
# 当执行到这一行时,程序会跳转到函数定义的位置,执行函数体中的命令
function_name
-
带参数的函数:
#!/bin/bash
# 定义一个名为 greet 的函数
# greet 函数用于输出一条带有特定参数的问候语
greet()
{
# 函数体中的命令,使用 echo 输出字符串
# "Hello, $1!" 是要输出的内容,其中 $1 是函数的第一个参数
# 当函数被调用时,$1 会被替换为实际传递的参数值
echo "Hello, $1!"
}
# 调用 greet 函数,并传递参数 "World"
# 此时函数内部的 $1 会被替换为 "World"
# 函数执行后会输出 "Hello, World!"
greet "World"
-
返回值:
#!/bin/bash
# 定义一个名为 add 的函数,用于计算两个数的和
add()
{
# 使用 local 关键字声明一个局部变量 sum
# 局部变量只在函数内部有效,避免与函数外部的同名变量冲突
# $(( $1 + $2 )) 是算术扩展语法,用于计算函数接收到的第一个参数($1)和第二个参数($2)的和
# 并将结果赋值给局部变量 sum
local sum=$(( $1 + $2 ))
# 使用 echo 命令输出变量 sum 的值,即两个参数的和
# 这个输出值可以在函数调用时被捕获
echo $sum
}
# 调用 add 函数,传递参数 5 和 3
# $(add 5 3) 是命令替换语法,会执行 add 函数,并将函数的输出结果(即 5 和 3 的和)赋值给变量 RESULT
RESULT=$(add 5 3)
# 使用 echo 命令输出带有变量 RESULT 的字符串
# 展示函数计算得到的结果
echo "Result: $RESULT"
4.2.4. 测试命令
测试命令用于检查文件属性、字符串比较等。
-
文件测试:
#!/bin/bash
# 指定脚本的解释器为 Bash,这意味着该脚本将由 /bin/bash 程序来执行
# 定义一个变量 file_path 来存储要检查的文件路径
# 这样可以方便地修改要检查的文件路径,提高代码的可维护性
# 变量名 file_path 具有明确的含义,方便理解其用途
# 变量赋值时,将文件路径 "/path/to/file" 赋给 file_path,实际使用时需替换为真实路径
file_path="/path/to/file"
# if 条件判断,检查 file_path 变量所指向的路径是否为一个普通文件
# if 是条件判断的起始关键字,开启一个条件判断结构
# [ -f "$file_path" ] 是条件测试部分,使用了 Shell 的测试命令 []
# -f 是测试选项,用于判断指定路径是否为普通文件
# "$file_path" 是对变量 file_path 的引用,将其值作为测试的路径
# 双引号的使用是为了防止路径中包含空格等特殊字符导致的问题
if [ -f "$file_path" ]; then
# 如果条件测试结果为真(即文件存在),则执行这一行的命令
# echo 是用于输出信息的命令
# "File exists." 是要输出的提示信息,表示文件存在
echo "File exists."
else
# 如果条件测试结果为假(即文件不存在),则执行这一行的命令
# 这里的 else 是条件判断中的分支关键字,用于处理条件不成立的情况
# 同样使用 echo 命令输出提示信息,表示文件不存在
echo "File does not exist."
fi
# fi 是条件判断语句的结束关键字,标志着 if 语句块的结束
-
字符串比较:
#!/bin/bash
# 这行代码指定了该脚本使用 /bin/bash 作为解释器。
# 当执行这个脚本时,系统会调用 Bash 来解析和执行脚本中的命令。
# 提示用户输入第一个字符串
# read 命令用于从标准输入(通常是键盘)读取用户输入的数据。
# -p 选项是 read 命令的一个参数,它的作用是在等待用户输入之前显示指定的提示信息。
# "请输入第一个字符串: " 就是提示信息,用户会看到这个提示并输入相应的字符串。
# STR1 是一个变量,用于存储用户输入的第一个字符串。当用户输入完成并按下回车键后,输入的内容会被赋值给 STR1。
read -p "请输入第一个字符串: " STR1
# 提示用户输入第二个字符串
# 同样使用 read 命令和 -p 选项,这次提示用户输入第二个字符串。
# 用户输入的内容会被赋值给变量 STR2。
read -p "请输入第二个字符串: " STR2
# if 条件判断,检查 STR1 和 STR2 是否相等
# if 是 Bash 中的条件判断关键字,用于开始一个条件判断结构。
# [ "$STR1" = "$STR2" ] 是条件测试部分,这里使用了 Shell 的测试命令 []。
# "$STR1" 和 "$STR2" 是对之前定义的变量的引用,使用双引号是为了避免变量值为空或包含特殊字符(如空格)时出现错误。
# = 是字符串比较运算符,用于判断两个字符串是否相等。如果相等,条件测试结果为真;否则为假。
if [ "$STR1" = "$STR2" ]; then
# 如果相等,输出字符串相等的提示信息
# echo 是用于输出信息的命令,会将 "Strings are equal." 这个字符串输出到标准输出(通常是终端)。
echo "Strings are equal."
else
# 如果不相等,输出字符串不相等的提示信息
# 当 if 后面的条件测试结果为假时,会执行 else 后面的代码块。
# 这里同样使用 echo 命令输出 "Strings are not equal." 提示用户两个字符串不相等。
echo "Strings are not equal."
fi
# fi 是条件判断语句的结束关键字,标志着整个 if-else 条件判断结构的结束。
-
数值比较:
#!/bin/bash
# 提示用户输入第一个数值
# read 命令用于从标准输入读取用户输入的数据
# -p 选项用于在读取输入之前显示提示信息
# 用户输入的内容会被赋值给变量 NUM1
read -p "请输入第一个数值: " NUM1
# 提示用户输入第二个数值
# 同样使用 read 命令和 -p 选项,用户输入的内容会被赋值给变量 NUM2
read -p "请输入第二个数值: " NUM2
# if 条件判断,检查 NUM1 是否大于 NUM2
# 使用双引号引用变量,避免变量值为空或包含特殊字符时出现错误
if [ "$NUM1" -gt "$NUM2" ]; then
# 如果 NUM1 大于 NUM2,输出相应的提示信息
echo "NUM1 is greater than NUM2."
# elif 用于在 if 条件不满足时进行额外的条件判断
# 检查 NUM1 是否小于 NUM2
elif [ "$NUM1" -lt "$NUM2" ]; then
# 如果 NUM1 小于 NUM2,输出相应的提示信息
echo "NUM1 is less than NUM2."
else
# 如果以上两个条件都不满足,说明 NUM1 等于 NUM2
# 输出相应的提示信息
echo "NUM1 is equal to NUM2."
fi
# fi 是条件判断语句的结束关键字,标志着整个 if-elif-else 结构的结束
4.3. 结构性语句(Structural Statements)
结构性语句用于组织和控制程序的结构,如脚本文件、命令分隔、输入输出重定向等。
4.3.1. 脚本文件
Shell 脚本是以 .sh
为扩展名的文本文件,通常以 #!/bin/bash
开头,指定解释器。
- 示例:
#!/bin/bash
# 这一行是 Shebang 行,它的作用是指定该脚本由哪个解释器来执行。
# /bin/bash 是 Bash 解释器的路径,意味着此脚本中的命令会由 Bash 进行解析和执行。
# 定义一个函数来输出问候语,提高代码的复用性
# print_greeting 是函数名,定义函数时使用这种函数名加括号的语法。
# 函数可以将一段代码封装起来,方便多次调用,提高代码的可维护性和复用性。
print_greeting()
{
# 函数内部使用 echo 命令输出问候语
# echo 是一个常用的命令,用于将文本输出到标准输出(通常是终端)。
# "Hello, World!" 是要输出的具体文本内容。
echo "Hello, World!"
}
# 调用 print_greeting 函数输出问候语
# 直接使用函数名即可调用函数,当执行到这一行时,程序会跳转到函数定义的地方执行函数体中的代码。
print_greeting
# 可以添加更多的功能,例如提示用户输入姓名并输出个性化问候语
# read 是一个用于从标准输入(通常是键盘)读取用户输入的命令。
# -p 选项的作用是在等待用户输入前显示提示信息。
# "请输入您的姓名: " 就是提示信息,用户看到这个提示后可以输入姓名。
# name 是一个变量,用户输入的内容会被赋值给这个变量。
read -p "请输入您的姓名: " name
# 判断用户是否输入了姓名
# if 是条件判断语句的起始关键字,用于开启一个条件判断结构。
# [ -n "$name" ] 是条件测试部分,使用了 Shell 的测试命令 []。
# -n 是一个测试选项,用于检查字符串的长度是否大于 0。
# "$name" 是对变量 name 的引用,使用双引号是为了防止变量值为空或者包含空格等特殊字符导致的问题。
if [ -n "$name" ]; then
# 如果用户输入了姓名,输出个性化问候语
# echo 命令将 "Hello, " 和变量 name 的值拼接后输出,实现个性化问候。
echo "Hello, $name!"
else
# 如果用户没有输入姓名,输出默认问候语
# 当 if 后面的条件不成立时,会执行 else 后面的代码块。
# 这里输出提示信息告知用户没有输入姓名。
echo "Hello! You didn't enter your name."
fi
4.3.2. 命令分隔
多个命令可以用分号 ;
或换行符分隔。
-
分号分隔:
#!/bin/bash
# 指定脚本的解释器为 /bin/bash,意味着该脚本中的命令将由 Bash 解释器来执行。
# 定义一个函数来执行命令并记录日志
# 函数名为 execute_commands,用于封装执行一系列命令和记录日志的操作,提高代码的复用性和可维护性。
execute_commands()
{
# 定义一个局部变量 log_file,用于存储日志文件的名称
# 局部变量仅在函数内部有效,避免与函数外部的同名变量冲突。
local log_file="command_log.txt"
# 使用 echo 命令将日志信息追加到日志文件中
# "开始执行命令: $(date)" 是要记录的日志内容,其中 $(date) 是命令替换,会将当前日期和时间插入到字符串中。
# >> 是追加输出重定向符号,将内容追加到指定文件的末尾。
echo "开始执行命令: $(date)" >> "$log_file"
# 执行 command1
# if 语句用于判断 command1 是否执行成功(返回状态码为 0)
if command1; then
# 如果 command1 执行成功,将成功信息和当前日期时间记录到日志文件中
echo "command1 执行成功: $(date)" >> "$log_file"
# 执行 command2
# 同样使用 if 语句判断 command2 是否执行成功
if command2; then
# 如果 command2 执行成功,将成功信息和当前日期时间记录到日志文件中
echo "command2 执行成功: $(date)" >> "$log_file"
# 执行 command3
# 再次使用 if 语句判断 command3 是否执行成功
if command3; then
# 如果 command3 执行成功,将成功信息和当前日期时间记录到日志文件中
echo "command3 执行成功: $(date)" >> "$log_file"
# 同时在终端输出所有命令执行成功的提示信息
echo "所有命令执行成功。"
else
# 如果 command3 执行失败,将失败信息和当前日期时间记录到日志文件中
echo "command3 执行失败: $(date)" >> "$log_file"
# 在终端输出 command3 执行失败的提示信息,提醒用户检查
echo "command3 执行失败,请检查。"
fi
else
# 如果 command2 执行失败,将失败信息和当前日期时间记录到日志文件中
echo "command2 执行失败: $(date)" >> "$log_file"
# 在终端输出 command2 执行失败的提示信息,提醒用户检查
echo "command2 执行失败,请检查。"
fi
else
# 如果 command1 执行失败,将失败信息和当前日期时间记录到日志文件中
echo "command1 执行失败: $(date)" >> "$log_file"
# 在终端输出 command1 执行失败的提示信息,提醒用户检查
echo "command1 执行失败,请检查。"
fi
# 无论命令执行结果如何,都将命令执行结束的信息和当前日期时间记录到日志文件中
echo "命令执行结束: $(date)" >> "$log_file"
}
# 调用函数执行命令
# 直接使用函数名 execute_commands 来调用上面定义的函数,开始执行命令并记录日志。
execute_commands
-
换行分隔:
#!/bin/bash
# 定义一个函数来执行命令并记录日志
execute_commands()
{
# 定义日志文件路径
local log_file="command_log.txt"
# 记录命令开始执行的时间
echo "开始执行命令: $(date)" >> "$log_file"
# 执行 command1 并检查返回状态码
if command1; then
# 若 command1 执行成功,记录成功信息和时间
echo "command1 执行成功: $(date)" >> "$log_file"
# 执行 command2 并检查返回状态码
if command2; then
# 若 command2 执行成功,记录成功信息和时间
echo "command2 执行成功: $(date)" >> "$log_file"
# 执行 command3 并检查返回状态码
if command3; then
# 若 command3 执行成功,记录成功信息和时间
echo "command3 执行成功: $(date)" >> "$log_file"
# 输出所有命令执行成功的提示
echo "所有命令执行成功。"
else
# 若 command3 执行失败,记录失败信息和时间
echo "command3 执行失败: $(date)" >> "$log_file"
# 输出 command3 执行失败的提示
echo "command3 执行失败,请检查。"
fi
else
# 若 command2 执行失败,记录失败信息和时间
echo "command2 执行失败: $(date)" >> "$log_file"
# 输出 command2 执行失败的提示
echo "command2 执行失败,请检查。"
fi
else
# 若 command1 执行失败,记录失败信息和时间
echo "command1 执行失败: $(date)" >> "$log_file"
# 输出 command1 执行失败的提示
echo "command1 执行失败,请检查。"
fi
# 记录命令执行结束的时间
echo "命令执行结束: $(date)" >> "$log_file"
}
# 调用函数执行命令
execute_commands
4.3.3. 输入输出重定向
输入输出重定向用于改变命令的标准输入、标准输出和标准错误流。
-
输出重定向:
# 执行 command 命令,并将其标准输出重定向到 output.txt 文件
# 如果 output.txt 文件不存在,会创建该文件;若存在,会覆盖原有内容
command > output.txt
-
追加输出:
# 执行 command 命令,并将其标准输出追加到 output.txt 文件中
# 如果 output.txt 文件不存在,会创建该文件;如果存在,会在文件末尾添加新的输出内容
command >> output.txt
-
输入重定向:
# 执行 command 命令,并将 input.txt 文件的内容作为该命令的标准输入
# 即 command 命令会从 input.txt 文件中读取数据进行处理
command < input.txt
-
错误重定向:
#!/bin/bash
# 指定脚本使用的解释器为 /bin/bash
# 定义错误日志文件
error_file="error.txt"
# 执行 command 命令,并将标准错误输出重定向到 error_file
command 2> "$error_file"
# 获取命令执行的退出状态码
exit_status=$?
if [ $exit_status -eq 0 ]; then
# 若退出状态码为 0,表示命令执行成功
echo "命令执行成功,未产生错误信息。"
else
# 若退出状态码不为 0,表示命令执行失败
echo "命令执行失败,错误信息已记录到 $error_file。"
fi
-
同时重定向输出和错误:
#!/bin/bash
# 指定脚本使用的解释器为 /bin/bash,当执行该脚本时,系统会调用 Bash 来解析和执行其中的命令。
# 定义日志文件
# 创建一个名为 log_file 的变量,将其值设置为 "output.log",这个变量将用于存储日志文件的名称。
log_file="output.log"
# 使用 &> 操作符重定向
# 执行 command 命令,这里的 command 代表用户要执行的具体命令,需要替换为实际命令。
# &> 是重定向操作符,它的作用是将 command 命令的标准输出(stdout)和标准错误输出(stderr)都重定向到指定的文件。
# "$log_file" 是重定向的目标文件,由于使用了双引号包裹变量,能确保变量值中包含特殊字符时也能正确处理。
# 如果 output.log 文件不存在,会自动创建该文件;如果文件已存在,原有内容会被覆盖。
command &> "$log_file"
# 获取命令执行的退出状态码
# $? 是一个特殊的变量,它会保存上一个执行的命令的退出状态码。
# 在 Unix 和类 Unix 系统中,通常 0 表示命令执行成功,非 0 值表示命令执行失败。
# 这里将 $? 的值赋给变量 exit_status,方便后续判断命令是否执行成功。
exit_status=$?
# 判断命令执行结果
# if 是条件判断语句的起始关键字,用于开启一个条件判断结构。
# [ ] 是测试命令,用于进行条件测试。
# -eq 是比较运算符,用于判断两个整数是否相等。
# 所以 [ $exit_status -eq 0 ] 表示判断 exit_status 变量的值是否等于 0。
if [ $exit_status -eq 0 ]; then
# 若退出状态码为 0,说明上一个命令执行成功,执行 then 后面的代码块。
# echo 是用于输出信息的命令,这里会在终端输出提示信息,告知用户命令执行成功且输出和错误信息已记录到指定的日志文件。
echo "命令执行成功,输出和错误信息已记录到 $log_file"
else
# 当 if 后面的条件不成立(即 exit_status 不等于 0)时,会执行 else 后面的代码块。
# 同样使用 echo 命令输出提示信息,告知用户命令执行失败,错误信息已记录到日志文件,并显示具体的退出状态码。
echo "命令执行失败,错误信息已记录到 $log_file,退出状态码: $exit_status"
fi
# fi 是条件判断语句的结束关键字,标志着整个 if - else 条件判断结构的结束。
4.3.4. 管道
管道用于将一个命令的输出作为另一个命令的输入。
-
示例:
#!/bin/bash
# 这是 Shebang 行,指定了该脚本使用的解释器为 /bin/bash。
# 当执行这个脚本时,系统会调用 Bash 来解析和执行脚本中的命令。
# 示例命令:查找当前目录下所有 .txt 文件,并统计这些文件的行数
# find 命令用于查找文件,这里查找当前目录(.)下所有扩展名为 .txt 的文件
# wc -l 命令用于统计行数,它会接收 find 命令的输出作为输入
find . -name "*.txt" | wc -l
# `find` 是一个强大的文件查找命令。
# `.` 表示当前目录,作为查找的起始路径。
# `-name` 是 `find` 命令的一个选项,用于指定按文件名进行匹配。
# `"*.txt"` 是一个通配符表达式,`*` 表示任意数量的任意字符,因此该表达式表示所有以 `.txt` 结尾的文件。
# `|` 是管道操作符,它的作用是将前一个命令(`find`)的标准输出(stdout)作为后一个命令(`wc -l`)的标准输入(stdin)。
# `wc -l` 是 `word count` 命令的一种用法,`-l` 选项表示只统计行数。
# 获取管道命令的退出状态
# ${PIPESTATUS[@]} 是一个数组,包含了管道中每个命令的退出状态
# ${PIPESTATUS[0]} 是第一个命令(find)的退出状态,${PIPESTATUS[1]} 是第二个命令(wc -l)的退出状态
exit_status1=${PIPESTATUS[0]}
exit_status2=${PIPESTATUS[1]}
# `${PIPESTATUS[@]}` 是一个特殊的数组变量,它会记录管道中每个命令的退出状态码。
# 退出状态码是一个整数,通常 0 表示命令执行成功,非 0 值表示命令执行失败。
# `exit_status1` 变量存储了管道中第一个命令(`find`)的退出状态码。
# `exit_status2` 变量存储了管道中第二个命令(`wc -l`)的退出状态码。
# 检查每个命令的执行结果
if [ $exit_status1 -eq 0 ] && [ $exit_status2 -eq 0 ]; then
# `if` 是条件判断语句的起始关键字,用于开启一个条件判断结构。
# `[ ]` 是测试命令,用于进行条件测试。
# `-eq` 是比较运算符,用于判断两个整数是否相等。
# `&&` 是逻辑与运算符,只有当左右两边的条件都为真时,整个条件才为真。
# 这里判断 `exit_status1` 和 `exit_status2` 是否都等于 0,如果都等于 0,则表示两个命令都执行成功。
echo "命令执行成功,已统计 .txt 文件的总行数。"
# 如果条件成立,使用 `echo` 命令输出提示信息,告知用户命令执行成功。
else
# 当 `if` 后面的条件不成立时,会执行 `else` 后面的代码块。
if [ $exit_status1 -ne 0 ]; then
# `-ne` 是比较运算符,用于判断两个整数是否不相等。
# 这里判断 `exit_status1` 是否不等于 0,如果不等于 0,则表示 `find` 命令执行失败。
echo "find 命令执行失败,退出状态码: $exit_status1"
# 如果 `find` 命令执行失败,使用 `echo` 命令输出提示信息,告知用户 `find` 命令执行失败,并显示具体的退出状态码。
fi
if [ $exit_status2 -ne 0 ]; then
# 判断 `exit_status2` 是否不等于 0,如果不等于 0,则表示 `wc -l` 命令执行失败。
echo "wc -l 命令执行失败,退出状态码: $exit_status2"
# 如果 `wc -l` 命令执行失败,使用 `echo` 命令输出提示信息,告知用户 `wc -l` 命令执行失败,并显示具体的退出状态码。
fi
fi
# `fi` 是条件判断语句的结束关键字,标志着整个 `if-else` 条件判断结构的结束。
-
多级管道:
#!/bin/bash
# 指定脚本使用的解释器为 /bin/bash
# 示例命令:查找当前目录下所有 .sh 文件,提取文件名,然后统计文件名的数量
# find 命令用于查找文件,这里查找当前目录(.)下所有扩展名为 .sh 的文件
# awk 命令用于提取文件名,通过 NF 变量获取每行最后一个字段(即文件名)
# wc -l 命令用于统计行数,也就是统计文件名的数量
find . -name "*.sh" | awk '{print $NF}' | wc -l
# 获取管道命令的退出状态
# ${PIPESTATUS[@]} 是一个数组,包含了管道中每个命令的退出状态
# ${PIPESTATUS[0]} 是第一个命令(find)的退出状态,${PIPESTATUS[1]} 是第二个命令(awk)的退出状态,${PIPESTATUS[2]} 是第三个命令(wc -l)的退出状态
exit_status1=${PIPESTATUS[0]}
exit_status2=${PIPESTATUS[1]}
exit_status3=${PIPESTATUS[2]}
# 检查每个命令的执行结果
if [ $exit_status1 -eq 0 ] && [ $exit_status2 -eq 0 ] && [ $exit_status3 -eq 0 ]; then
echo "命令执行成功,已统计 .sh 文件的数量。"
else
if [ $exit_status1 -ne 0 ]; then
echo "find 命令执行失败,退出状态码: $exit_status1"
fi
if [ $exit_status2 -ne 0 ]; then
echo "awk 命令执行失败,退出状态码: $exit_status2"
fi
if [ $exit_status3 -ne 0 ]; then
echo "wc -l 命令执行失败,退出状态码: $exit_status3"
fi
fi
4.3.5. 子 shell 和后台进程
子 shell 和后台进程用于并行执行任务。
-
子 shell:
#!/bin/bash
# 指定脚本使用的解释器为 /bin/bash
# 示例命令:在子 shell 中依次执行两个 sleep 命令,模拟耗时任务
# sleep 命令用于让脚本暂停执行指定的时间,这里分别暂停 3 秒和 2 秒
(sleep 3; sleep 2) &
# 记录子 shell 的进程 ID
subshell_pid=$!
# 输出子 shell 的进程 ID
echo "子 shell 进程 ID: $subshell_pid"
# 等待子 shell 执行完毕
wait $subshell_pid
# 获取子 shell 的退出状态
exit_status=$?
# 检查子 shell 的执行结果
if [ $exit_status -eq 0 ]; then
echo "子 shell 中的命令执行成功,退出状态码: $exit_status"
else
echo "子 shell 中的命令执行失败,退出状态码: $exit_status"
fi
-
后台进程:
#!/bin/bash
# 指定脚本使用的解释器为 /bin/bash
# 示例命令,使用 sleep 命令模拟一个耗时任务,这里暂停 5 秒
sleep 5 &
# $! 是一个特殊的变量,它会保存最近一个放到后台执行的进程的进程 ID(PID)
# 这里将 sleep 命令对应的后台进程的 PID 保存到变量 background_pid 中
background_pid=$!
# 输出后台进程的 PID,方便用户查看和管理
echo "后台进程的 PID 是: $background_pid"
# wait 命令用于等待指定 PID 的进程执行完成
# 这里等待 background_pid 对应的后台进程执行完毕
wait $background_pid
# $? 是一个特殊变量,用于获取上一个命令的退出状态码
# 这里将后台进程的退出状态码保存到变量 exit_status 中
exit_status=$?
# 判断后台进程的执行结果
if [ $exit_status -eq 0 ]; then
# 若退出状态码为 0,表示后台进程执行成功
echo "后台进程执行成功,退出状态码: $exit_status"
else
# 若退出状态码不为 0,表示后台进程执行失败
echo "后台进程执行失败,退出状态码: $exit_status"
fi
-
等待所有后台进程完成:
#!/bin/bash
# 这是 Shebang 行,指定该脚本使用 /bin/bash 作为解释器。
# 当脚本被执行时,系统会调用 Bash 来解析和执行脚本中的命令。
# 启动多个后台进程,这里使用 sleep 命令模拟耗时任务
sleep 3 &
# `sleep 3` 命令会让进程暂停 3 秒。
# `&` 符号将 `sleep 3` 命令放到后台执行,这样脚本不会等待该命令执行完毕,而是继续执行后续命令。
sleep 2 &
# 同样,`sleep 2` 会让进程暂停 2 秒,`&` 使其在后台运行。
sleep 4 &
# `sleep 4` 会让进程暂停 4 秒,`&` 使其在后台运行。
# 记录所有后台进程的 PID 到数组中
pids=($!)
# `$!` 是一个特殊变量,它会保存最近一个放到后台执行的进程的进程 ID(PID)。
# 这里将最后一个启动的后台进程(即 `sleep 4`)的 PID 赋值给数组 `pids` 的第一个元素。
while [ -n "$(jobs -rp)" ]; do
# `jobs -rp` 命令用于列出所有正在运行(`-r` 选项)的后台进程(`-p` 选项表示只输出 PID)。
# `[ -n STRING ]` 是测试条件,用于判断字符串 `STRING` 是否非空。
# 此 `while` 循环会持续执行,只要还有正在运行的后台进程。
# 持续获取新的后台进程 PID 并添加到数组中
pids+=($(jobs -rp))
# `+=` 是数组的追加操作符,将 `jobs -rp` 输出的所有 PID 追加到数组 `pids` 中。
sleep 0.1
# `sleep 0.1` 让脚本暂停 0.1 秒,避免过于频繁地检查后台进程,减少系统资源消耗。
done
# 输出所有后台进程的 PID
echo "所有后台进程的 PID: ${pids[@]}"
# `echo` 是输出命令,用于在终端显示信息。
# `${pids[@]}` 表示数组 `pids` 中的所有元素。
# 这行代码会输出所有记录的后台进程的 PID。
# 等待所有后台进程完成
wait
# `wait` 命令会暂停脚本的执行,直到所有后台子进程都执行结束。
# 检查每个后台进程的执行结果
for pid in "${pids[@]}"; do
# `for` 循环用于遍历数组 `pids` 中的每个元素(即每个后台进程的 PID)。
# 获取每个进程的退出状态码
exit_status=$(ps -p $pid -o stat=)
# `ps -p $pid -o stat=` 命令用于获取指定 PID(`$pid`)进程的状态信息。
# `-p` 选项指定要查询的进程 PID,`-o stat=` 选项表示只输出进程的状态,且不显示标题行。
# 将查询到的状态信息赋值给变量 `exit_status`。
if [ -z "$exit_status" ]; then
# `[ -z STRING ]` 是测试条件,用于判断字符串 `STRING` 是否为空。
# 如果 `ps` 命令没有返回状态信息,说明进程已经结束,且可能是正常结束。
# 如果 ps 命令没有返回状态,说明进程已正常结束
echo "进程 $pid 执行成功"
# 输出提示信息,表示该进程执行成功。
else
# 否则,进程可能异常结束
echo "进程 $pid 执行失败,退出状态: $exit_status"
# 输出提示信息,表示该进程执行失败,并显示其退出状态。
fi
done
4.3.6. 捕获信号
捕获信号用于处理中断和其他系统事件。
-
捕获信号:
#!/bin/bash
# 指定脚本使用的解释器为 /bin/bash,确保脚本在 Bash 环境下运行
# 定义一个信号处理函数,用于处理 SIGINT 信号
# 函数名为 handle_sigint,当接收到 SIGINT 信号时会执行此函数
handle_sigint()
{
# 当函数被调用时,会在终端输出提示信息,表示脚本被中断
echo "Script interrupted"
# 执行 exit 命令,终止当前脚本的执行
exit
}
# 使用 trap 命令捕获 SIGINT 信号,并调用 handle_sigint 函数进行处理
# trap 是 Bash 内置命令,用于捕获指定信号并执行相应操作
# handle_sigint 是前面定义的信号处理函数
# SIGINT 是信号名称,代表中断信号,通常由用户按下 Ctrl + C 触发
# 当脚本接收到 SIGINT 信号时,会调用 handle_sigint 函数进行处理
trap handle_sigint SIGINT
# 进入一个无限循环,每次循环暂停 1 秒
# while true 是一个无限循环的结构,因为 true 命令的返回值始终为真
# 只要条件为真,循环体就会不断执行
while true; do
# sleep 1 命令让脚本暂停执行 1 秒
# 这样可以避免循环过于频繁地执行,减少系统资源的消耗
sleep 1
done