文章目录
- The Missing Semester of Your CS Education 学习笔记以及一些拓展知识
- Bash脚本
- 笔记部分
- 一些在Bash脚本中的常用命令补充
- 常用标准输入输出命令
- 常用环境变量(普通变量)控制命令
- 常用系统时间信息获取命令
- 常用函数执行状态控制命令
- 常用脚本执行控制命令
- Bash脚本的创建和运行
- Bash脚本的SheBang
- Bash脚本的变量
- 变量的定义
- 变量的使用
- 变量的类型
- 局部变量
- 环境变量
- 基本内置变量(位置参数)
- 特殊内置变量
- Bash变量的三个特殊机制
- 数组
- 命令替换 (Command Substitution)
- 变量扩展(参数拓展)
- 变量使用时的注意事项
- Bash脚本的条件结构
- test命令
- test的基本功能
- test指令的三种形式
- test的常用测试表达式
- 组合条件
- test的使用基本注意事项
- let命令
- let的基本功能
- let支持的运算符
- let的改进写法:双括号算术运算 ((...))
- if-then结构
- if的三种结构
- if的判断条件类型
- if的一些使用注意
- case结构
- case的基本结构
- case使用的一些注意事项
- &&和||
- Bash脚本的循环结构
- for循环
- for循环的两种基本结构
- for循环的注意事项
- while循环
- while的基本语法:
- while的常见用法
- while的注意事项
- until循环
- 循环控制指令
- break
- continue
- Bash脚本的函数
- 函数的定义
- 函数的调用
- 函数的传参
- 函数变量的作用域
- 函数的执行结果
- 总结函数的注意事项
- Bash脚本的Debug
- 语法检查
- 脚本逻辑的debug
- 其他
- 习题部分
The Missing Semester of Your CS Education 学习笔记以及一些拓展知识
以下是使用The Missing Semester of Your CS Education的个人学习笔记,方便之后翻阅
Bash脚本
上一节我们学习了简单的命令。假设有这么一种情况:我们希望每次开机后都执行一串相同的命令,难道说我们只能一个命令一个命令地在终端里面敲吗?答案是我们可以把这些重复命令编写成shell脚本,以下是二者的主要区别:
- 交互性 vs. 自动化
命令行是为交互而生的。你输入一个命令,马上就能看到结果,然后根据结果决定下一步做什么。这个即时反馈的循环对于文件管理、系统监控和问题排查至关重要。
脚本是为自动化而生的。它的设计初衷就是“一次编写,多次运行”。对于那些需要重复执行的、由多个步骤组成的任务(例如:每日备份、部署网站、批量转换文件),写成脚本可以极大地提高效率并减少人为错误。 - 编程能力上,Bash脚本引入了非常多的语法结构,支持编写复杂的逻辑功能。
- 运行环境的差别(非常重要)
命令行中的命令在当前 Shell 环境中执行。你在命令行里做的任何环境改变(如用 cd 切换目录、用 export 设置环境变量),都会立即对你当前的 Shell 生效。
脚本默认在一个新的子 Shell (subshell) 中执行。这意味着脚本内部对环境的修改(如 cd、变量赋值)不会影响到执行它的那个父 Shell。
举个例子:你在命令行里现在位于 /home/user。你有一个脚本 test.sh,内容是 cd /tmp。你执行 ./test.sh。脚本执行时,它的工作目录确实切换到了 /tmp。但当脚本执行完毕,你回到命令行时,你所在的目录仍然是 /home/user,而不是 /tmp。因为 cd 命令只在那个短暂存在的子 Shell 里生效了。
例外:如果你使用 source 命令(或其简写 .)来执行脚本,那么脚本会在当前 Shell 中执行,其内部的环境变量和目录切换会保留下来。例如:source test.sh。
笔记部分
一些在Bash脚本中的常用命令补充
常用标准输入输出命令
命令 | 全称 | 基本功能 | 常用参数 |
---|---|---|---|
read | read | 从标准输入(stdin)中读取一行文本,并将其赋值给一个或多个变量 | -p(prompt):在读取输入前,先显示指定的提示信息。常用于交互式脚本。 -r (raw read):原始读取模式。禁止反斜杠 \ 的转义功能。 -s (silent):静默模式。不将用户输入的内容显示在屏幕上。非常适合用于输入密码或敏感信息。 -t <秒数> :设置一个超时时间。如果在指定秒数内用户没有输入,read 命令会失败并返回一个非零状态码。 -n <字符数> :读取指定数量的字符后立即返回,而无需等待用户按回车。 |
echo | echo(回声) | 在标准输出(stdout)上打印文本或变量内容。 | -n :输出内容后不自动添加换行符。 -e :启用对反斜杠转义字符的解释。 |
crul | See URL | 默认将从URL获取的响应打印到标准输出 | -s (silent):静默模式。不显示进度条和错误信息。在脚本中强烈推荐使用。 -L (Location):自动跟随重定向。如果请求的 URL 返回一个3xx重定向,curl 会自动请求新的 URL。 -o <文件名> (output):将下载内容写入指定文件,而不是标准输出。 -O:将下载内容以 URL 中的原始文件名保存在当前目录。 -I (Information):只获取 HTTP 响应头(HEAD请求)。 -X <方法> (e.g., POST, PUT, DELETE):指定 HTTP 请求方法 -H “<头部信息>” (Header):添加自定义的 HTTP 请求头。 -d “<数据>” (data):发送 POST 请求的数据体。 |
crul有一个姊妹命令wget(web get),与 curl 不同,它的默认行为是将内容保存到文件而非stdio。
常用环境变量(普通变量)控制命令
命令 | 全称 | 基本功能 | 常用参数 |
---|---|---|---|
env | environment | 显示所有环境变量或者进行一些环境变量临时设置 | env: 直接执行,列出当前所有的环境变量,每行一个 变量名=值。 -i表示忽略当前的所有环境变量,在一个完全干净的空环境中执行后续命令。 env <变量=值> <命令>: 临时为 <命令> 设置一个环境变量,但不影响当前的 Shell 环境。 |
printenv | print environment | 打印环境变量的值 | printenv: 不带参数时,行为与 env 几乎完全相同,列出所有环境变量。 printenv <变量名>: 只打印指定单个环境变量的值。 |
export | export | 将一个变量“输出”到环境中,用于创建新的环境变量,或者将一个已经存在的局部变量提升(或称“导出”)为环境变量。 | export 变量名=值: 定义一个变量并立即将其导出为环境变量(最常用)。 变量名=值; export 变量名: 先定义一个局部变量,再将其导出。 -p: 打印出当前 Shell 中所有被导出的环境变量,其输出格式是可被 Shell 重新执行的 export 变量名=“值” 形式。单独执行 export -p 的效果与 env 类似。 -n: 将一个已存在的环境变量降级为一个局部变量,它将不再被子进程继承 |
unset | unset | 从当前 Shell 环境中彻底删除一个变量或函数 | -v (variable):明确表示要删除的是一个变量(这是默认行为,通常可省略)。 -f (function):明确表示要删除的是一个函数。 |
常用系统时间信息获取命令
命令 | 全称 | 基本功能 | 常用参数 |
---|---|---|---|
date | date | 获取系统时间或者设置系统时间 | +Format:自定义日期形式,如:date +‘%Y-%m-%d %H:%M:%S’ -s 设置系统时间 -d 计算相对时间 |
关于获取系统的硬件信息和系统信息之后再说吧
常用函数执行状态控制命令
命令 | 全称 | 基本功能 | 常用参数 |
---|---|---|---|
return | return | 立即终止当前正在执行的函数并为该函数设置一个退出状态码 ($?) | return [n],n 是一个可选的整数参数,范围是0到255。如果省略n,函数的退出状态码将是函数中最后一条被执行的命令的退出状态码。 |
true | true | 不做任何事,返回状态码0 | 无 |
false | false | 不做任何事,返回状态码1 | 无 |
常用脚本执行控制命令
命令 | 全称 | 基本功能 | 常用参数 |
---|---|---|---|
exit | exit | 立即终止整个脚本的执行,并可以为脚本设置一个最终的退出状态码 ($?)。 | exit [n]如果省略 n,则脚本的退出状态码是 exit 命令之前最后一条被执行的命令的退出状态码。 |
sleep | sleep | 让脚本的执行暂停指定的一段时间。 | sleep <数字>[后缀],后缀可以是s、m、h、d |
shift | shift | 将参数列表向左“移动”一位。原来的$1会被丢弃(不可恢复),$2变成新的$1,$3变成新的 $2,以此类推。同时,参数总数 $# 的值也会减 1。 | shift [n],n 是一个可选的数字,表示要移动的位数。如果省略,默认为 1。 |
source | source | 在当前的Shell环境中读取并执行一个脚本文件中的命令,而不是为该脚本启动一个新的子 Shell。 | 无 |
exec | execute | 使用一个新的命令来替换当前的 Shell 进程。 | 无 |
关于更进一步的一些进程控制命令如kill、jobs、fg、bg之后再说吧
Bash脚本的创建和运行
还是HelloWorld程序起手:
touch helloworld.sh
nano helloworld.sh# 编写如下内容,保存后退出
#!/bin/bash
echo "Hello World"chmod u+x helloworld.sh #更改权限
./helloworld.sh
从这个小小的例子可以看出:创建文件、编写代码、赋予权限、执行是所有 Bash 脚本开发的基础流程。
Bash脚本的SheBang
Shebang,即#(sharp,但简写成she)+!(Bang)是位于脚本文件第一行开头的一个特殊字符序列。它的作用是向操作系统(OS)的程序加载指明当用户尝试直接执行这个文件时,请不要把它当作普通的二进制文件,而是应该用我后面指定的这个解释器来运行。
基本语法:
#!/path/to/interpreter [optional-argument]
- #!: 这两个字符是固定不变的“魔术字符”,内核通过它们识别这是一个需要解释器执行的脚本。
- /path/to/interpreter: 这是解释器程序的绝对路径。对于 Bash 脚本,它通常是 /bin/bash。
- [optional-argument]: 这是一个可选的参数,会传递给解释器,一般情况下省略。
例如:
#!/bin/bash
#!/bin/sh
#!/usr/bin/python3
#!/usr/bin/node
以上分别是bash、通用shell、python和Node.js脚本的解释器声明。
如果你不写 Shebang,直接运行 ./myscript.sh,那么你当前的 Shell(比如你正在使用的 Bash 或 Zsh)会尝试去逐行解释执行这个脚本,就可能会因为语法不兼容而出错。
Bash脚本的变量
变量的定义
基本语法和示例
var=value# 字符串变量
greeting="Hello, World!"# 整数变量(本质上仍是字符串)
cnt=10
定义时注意事项
- 等号两边不能有空格! 这是初学者最常犯的错误。VAR = “value” 是错误的,Shell 会把 VAR 当成一个命令来执行。
- 变量名规范:通常由字母、数字和下划线组成,不能以数字开头。习惯上,环境变量和全局变量使用全大写,脚本内部的局部变量使用小写,以作区分。
- 无需声明类型:Bash 中的变量默认都是字符串类型。即使你赋值一个数字 10,它本质上也是字符串 “10”,但在进行数学运算时,Shell 会自动进行转换。
变量的使用
基本语法和示例:
$变量名 或者 ${变量名}
# 更推荐后者写法name="Alex"
echo "My name is $name"
echo "My name is ${name}"
花括号 {} 是一种更严谨和安全的写法,它可以明确变量名的边界,避免歧义。看下面的例子:
fruit="apple"
echo "I have five ${fruit}s" # 正确输出: I have five apples
echo "I have five $fruits" # 错误输出: I have five (因为 Shell 试图寻找一个叫 fruits 的变量,但它不存在)
变量的类型
在Bash中变量本质上都被视作字符串,所以下面分类依据是作用域和来源。
局部变量
在脚本中直接定义的变量,其作用域默认为整个脚本。如果在函数内部定义,为了防止污染外部作用域,应使用local关键字声明。
#!/bin/bash
name="global"my_func() {local name="local" # 使用 local 关键字,此变量只在函数内有效echo "Function sees name as: $name"
}echo "Before function call, name is: $name"
my_func
echo "After function call, name is: $name" # 外部变量不受函数内部影响# 输出
Before function call, name is: global
Function sees name as: local
After function call, name is: global
环境变量
这些变量对当前 Shell 会话以及由它启动的所有子进程(包括你运行的脚本)都可见。它们通常用于配置系统环境。环境变量可以自己利用report工具定义,也有如下常见的内置环境变量:
环境变量 | 说明 |
---|---|
$HOME | 当前用户的夹目录 |
$PATH | 可执行文件的搜索路径 |
$USER | 用户名 |
$PWD | 当前工作目录 |
$HOSTNAME | 系统主机名称 |
$LANG | 系统语言和编码 |
基本内置变量(位置参数)
这些变量由 Shell 自动赋值,用于获取传递给脚本的命令行参数。
- $0: 脚本本身的文件名。
- $1, $2, $3…: 分别代表第 1、第 2、第 3 个参数。
- ${10}, ${11}… 当参数超过 9 个时,必须用花括号 {} 包裹。
- $#: 传递给脚本的参数总个数。
- $@: 代表所有参数的列表,每个参数都是独立的字符串。在循环中,“$@” 是最常用和最安全的方式。
- $*: 代表所有参数组成的单个字符串。
#!/bin/bash
# 用法: ./backup.sh /path/to/source /path/to/destinationSOURCE_DIR=$1
DEST_DIR=$2echo "将从 '$SOURCE_DIR' 备份到 '$DEST_DIR'..."
cp -r "$SOURCE_DIR" "$DEST_DIR"
这里特别注意一下$@与$*的区别:
当不使用双引号时,$@ 和 $* 的表现是一样的。但一旦用双引号包裹,它们的行为就截然不同。
- “$*”:会将所有参数视为一个整体的字符串。“param1 param2 param3”。
- “$@”:会将每个参数视为独立的字符串。“param1” “param2” “param3”
例如脚本loop_test.sh:
#!/bin/bash
echo "--- 循环遍历 \"\$*\" (单个字符串) ---"
for arg in "$*"; doecho "参数: $arg"
doneecho ""
echo "--- 循环遍历 \"\$@\" (独立字符串列表) ---"
for arg in "$@"; doecho "参数: $arg"
done## 执行
chmod +x loop_test.sh
./loop_test.sh "first arg" "second" "third"## 结果
--- 循环遍历 "$*" (单个字符串) ---
参数: first arg second third--- 循环遍历 "$@" (独立字符串列表) ---
参数: first arg
参数: second
参数: third
在需要遍历或传递参数列表时,99% 的情况下你都应该使用 “$@”。它能保证参数(即使包含空格)被正确、独立地处理。
特殊内置变量
这些变量由 Shell 预设,用于提供脚本运行状态的信息。
- $?: 上一个命令的退出状态码。这是脚本中进行错误检查的最重要变量。0代表成功。非0代表失败。
- $$:当前脚本的进程ID。常用于创建唯一的临时文件名。
- $!: 上一个在后台运行的命令的进程 ID。
cp source.txt dest.txt
if [ $? -eq 0 ]; thenecho "文件复制成功!"
elseecho "文件复制失败!"
fi
Bash变量的三个特殊机制
数组
Bash 支持一维数组来存储一组值。
- 定义数组: array=(“apple” “banana” “cherry”)
- 获取元素: ${array[0]} (获取第一个元素,索引从0开始)
- 获取所有元素: ${array[@]}
- 获取数组长度: ${#array[@]}
SERVERS=("server1.com" "server2.com" "server3.com")for server in "${SERVERS[@]}"; doecho "正在 ping 服务器: $server"ping -c 1 "$server"
done
命令替换 (Command Substitution)
这是一种非常强大的机制,允许你将一个命令的输出结果赋值给一个变量。
基本语法和示例:
var=$(command) 或者 VAR=`command`# 获取当前日期
TODAY=$(date +%Y-%m-%d)
echo "今天是: $TODAY"# 获取 txt 文件数量
FILE_COUNT=$(find . -type f -name "*.txt" | wc -l)
echo "这里有 $FILE_COUNT 个 txt 文件。"
推荐使用()而非``,因为使用 $(…) 的好处是它可以轻松地嵌套,而反引号`嵌套起来非常麻烦和混乱。
变量扩展(参数拓展)
Bash 提供了强大的参数扩展功能,可以在不使用外部命令的情况下对变量进行处理。
-
提供默认值(提升代码健壮性)
- ${variable:-default}: 如果 variable 未定义或为空,则返回 default,但不改变 variable 本身的值。
- ${variable:=default}: 如果 variable 未定义或为空,则返回 default,并同时将 default 赋值给 variable。
-
字符串操作
- ${#variable}: 获取变量值的长度。
- ${variable#pattern}: 从开头删除最短匹配 pattern 的部分。
- ${variable##pattern}: 从开头删除最长匹配 pattern 的部分。
- ${variable%pattern}: 从结尾删除最短匹配 pattern 的部分。
- ${variable%%pattern}: 从结尾删除最长匹配 pattern 的部分。
- ${variable/pattern/string}: 替换第一个匹配的 pattern。
- ${variable//pattern/string}: 替换所有匹配的 pattern。
# 默认值
echo "未定义的用户: ${UNSET_USER:-guest}" # 输出:未定义的用户: guest# 字符串操作
FILEPATH="/home/user/document.txt"
echo "文件名: ${FILEPATH##*/}" # 输出:document.txt (删除最长的前缀 */)
echo "目录名: ${FILEPATH%/*}" # 输出:/home/user (删除最短的后缀 /*)
echo "扩展名: ${FILEPATH##*.}" # 输出:txt
变量使用时的注意事项
- 永远用双引号包裹变量:使用$variable 或 ${variable} 时,请始终用双引号包裹,即 “$variable”。这可以防止当变量值包含空格或特殊字符时,发生意想不到的“分词”和“文件名扩展”问题。这是编写健壮脚本的黄金法则。
- 赋值时等号两边无空格:再次强调,var=“value” 是正确的,其他形式都是错误的。
- 优先使用 ${}:使用 ${variable} 的形式可以使代码更清晰,并避免变量名边界的歧义。
- 检查命令成功与否:在执行一个重要命令后,检查 $? 的值,以确保脚本在出错时能妥善处理,而不是继续盲目执行。
- 区分环境变量和局部变量:使用大写命名环境变量,使用小写命名脚本内部的变量,可以提高代码的可读性。在函数内部尽量使用 local 关键字。
Bash脚本的条件结构
test命令
test的基本功能
test 是一个标准的、内建于 Shell 的命令。它的核心用途只有一个:计算一个表达式的真伪。
强调一下是表达式的真伪而非命令执行的情况
- 如果表达式为真 (true),test 命令执行成功,并返回退出状态码 0。
- 如果表达式为假 (false),test 命令执行失败,并返回一个非 0 的退出状态码(通常是 1)。
它的输出不是文本"true"或"false",而是通过退出状态码 ($?) 来报告结果。这正是if语句所需要的判断依据。
# 检查文件 /etc/hosts 是否存在 (这个文件通常存在)
test -f /etc/hosts
echo $? # 输出: 0 (代表 true)# 检查一个不存在的文件
test -f /nonexistent/file
echo $? # 输出: 1 (代表 false)
test指令的三种形式
- test expression (经典形式)
这是 test 命令最原始的形态,现在已不常用,但了解它有助于理解本质。
if test -f "/etc/hosts"; thenecho "文件存在。"
fi
- [ expression ] (POSIX 标准形式)
是test命令的等效写法,一个方括号 [。它在功能上与 test 完全等价。
if [ -f "/etc/hosts" ]; thenecho "文件存在。"
fi
注意:
-
[ 实际上是一个命令的名称,和 test 一样。
-
[ 后面和 ] 前面必须有空格! 这是强制性的语法要求。可以理解为 [ 文件存在吗 /etc/hosts ],每个部分都是独立的参数。[ -f …] 是错误的。
- [[ expression ]] (Bash 扩展形式,推荐使用)
在编写 Bash 脚本时,强烈推荐使用它。它解决了 [ 的一些不足。
if [[ -f "/etc/hosts" ]]; thenecho "文件存在。"
fi
[[ … ]] 相比于 [ … ] 的优势:
- 更安全的变量处理:在 [[ … ]] 中,即使变量包含空格或为空,不加双引号通常也不会导致错误。而在 [ … ] 中,不加双引号的变量是导致脚本错误的常见原因。尽管如此,为变量加上双引号永远是最佳实践。
- 更直观的逻辑运算:可以直接使用 && (与), || (或),而不是 -a, -o。
- 支持模式匹配和正则:可以用 == 和 != 进行通配符匹配(globbing),用 =~ 进行正则表达式匹配。
test的常用测试表达式
- 文件测试
这是脚本中最常见的测试类型,用于检查文件或目录的状态。
表达式 | 描述 |
---|---|
-e <路径> | exists - 路径是否存在(文件或目录均可)。 |
-f <路径> | file - 路径是否存在且为一个普通文件。 |
-d <路径> | directory - 路径是否存在且为一个目录。 |
-s <路径> | size - 文件是否存在且大小不为零。 |
-r <路径> | readable - 文件是否存在且当前用户可读。 |
-w <路径> | writable - 文件是否存在且当前用户可写。 |
-x <路径> | xecutable - 文件是否存在且当前用户可执行。 |
CONFIG_FILE="/etc/myapp.conf"if [[ -f "$CONFIG_FILE" && -r "$CONFIG_FILE" ]]; thenecho "正在读取配置文件..."# ... 读取文件内容 ...
elseecho "错误:配置文件不存在或不可读!" >&2exit 1
fi
- 字符串测试
表达式 | 描述 |
---|---|
$str1" = “$str2” | 字符串内容是否相等。(== 在 [ 和 [[ 中效果相同) |
“$str1” != “$str2” | 字符串内容是否不相等。 |
-z “$str” | zero length - 字符串长度是否为零(即是否为空)。 |
-n “$str” | non-zero length - 字符串长度是否不为零(即是否非空)。 |
read -p "请输入您的名字: " user_nameif [[ -z "$user_name" ]]; thenecho "错误:名字不能为空!"
elseecho "你好, $user_name!"
fi
- 整数测试
用于比较整数大小时,必须使用下面的专属操作符。
表达式 | 描述 |
---|---|
$int1 -eq $int2 | equal - 是否相等。 |
$int1 -ne $int2 | not equal - 是否不相等。 |
$int1 -gt $int2 | greater than - 是否大于。 |
$int1 -ge $int2 | greater than or equal - 是否大于等于。 |
$int1 -lt $int2 | less than - 是否小于。 |
$int1 -le $int2 | less than or equal - 是否小于等于。 |
# 检查传入脚本的参数数量是否为 2
if [ "$#" -ne 2 ]; thenecho "用法: $0 <源文件> <目标文件>"exit 1
fi
组合条件
- 在 [ … ] 中(不推荐)
- -a: and (与)
- -o: or (或)
- !: not (非)
# 判断 num 是否在 0 到 100 之间
if [ "$num" -ge 0 -a "$num" -le 100 ]; thenecho "有效范围"
fi
- 在 [[ … ]] 中(推荐)
- &&: and (与)
- ||: or (或)
- !: not (非)
# 同样是判断 num 是否在 0 到 100 之间,可读性更高
if [[ "$num" -ge 0 && "$num" -le 100 ]]; thenecho "有效范围"
fi
test的使用基本注意事项
- 优先使用 [[ … ]]:在编写 Bash 脚本时,只要不需要考虑兼容古老的 sh,就应该优先使用 [[ … ]],它更健壮、功能更强、语法更友好。
- 永远用双引号包裹变量:在进行字符串比较或文件测试时,务必将变量用双引号 “” 包裹起来,即 [[ -f “file"]]和[["file" ]] 和 [[ "file"]]和[["str1” == “$str2” ]]。这可以防止当变量为空或包含空格时,test 命令解析出错。
- 区分字符串和整数比较:比较字符串用 == 或 =,比较整数用 -eq, -gt 等。混用会导致意想不到的结果。
- test的结果是状态码:牢记 test 命令本身不产生任何可见输出,它的价值在于其退出状态码 $?,if 和 while 语句正是依赖这个状态码来工作的。
let命令
let的基本功能
在 Bash Shell 中,变量默认是被当作字符串来处理的。这意味着你不能直接用常规的方式进行数学计算。let 命令的核心作用就是告诉Bash把接下来的表达式当作一个或多个整数算术运算来处理,而不是当作字符串。
let的基本语法结构和示例:
let <算术表达式1> [算术表达式2] ...a=10
b=5let result=a+b # 加法
echo $result # 输出: 15
let result=a-b # 减法
echo $result # 输出: 5
let result=a*b # 乘法
echo $result # 输出: 50
let result=a/b # 除法 (整数除法)
echo $result # 输出: 2
let result=a%b # 取余
echo $result # 输出: 0
let表达式有一个突出的优点,那就是变脸引用可以不加上$:
a=10
b=5
let result=a+b # 推荐,更简洁
let result=$a+$b # 也可以,效果相同
但let也有一个突出的缺点,如果你的表达式中包含空格,必须用引号将整个表达式引起来。
# 错误写法,Shell 会把 =、5、+、3 当成多个独立参数
let x = 5 + 3 # 正确写法
let "x = 5 + 3"
echo $x # 输出: 8
let支持的运算符
- 赋值:=
- 算术:+ (加), - (减), * (乘), / (除), % (取余), ** (幂运算)
- 自增/自减:var++ (后增), var-- (后减), ++var (前增), --var (前减)
- 复合赋值:+=, -=, *=, /=, %=
- 位运算:<< (左移), >> (右移), & (按位与), | (按位或), ^ (按位异或), ~ (按位非)
- 逻辑运算:! (逻辑非), && (逻辑与), || (逻辑或), , (逗号,用于分隔多个表达式)
- 三元运算符:?: (例如 let “a = 1 > 0 ? 1 : 0” )
let的改进写法:双括号算术运算 ((…))
这是目前最推荐的进行整数运算的方式。它是一个命令,也可作为一个表达式。
- 作为命令,进行赋值:(( 算术表达式 ))
- 作为表达式,获取结果:$(( 算术表达式 ))
a=10
b=5# 1. 赋值
(( c = a + b * 2 ))
echo $c # 输出: 20# 2. 获取结果
result=$(( a + b ))
echo $result # 输出: 15# 3. 在 if 语句中直接使用
if (( a > b )); thenecho "a 大于 b"
fi
注意:
- ((…))相比于let的巨大优势:表达式内可以随意使用空格,不需要引号;
- 变量引用无需$:和let 一样,内部的变量无需 $ 前缀。
- 使用((…))处理整数判断,进而替代[[…]]的一部分功能,使用起来更接近C语言,可以直接用于流程控制:if 和 while 语句可以直接使用双括号进行判断。
if-then结构
首先还是要强调if语句的核心本质:检查命令的退出状态码。在很多编程语言(如 Python,Java)中,if 后面跟的是一个布尔值(true/false)。但在 Bash 中,if 的工作方式有本质不同:if 后面跟的是一个或多个 命令,它检查的是这些命令执行后的 退出状态码 (Exit Status)。
# -q 选项让 grep "安静"执行,不输出任何内容,只通过退出状态码报告结果
if grep -q "root" /etc/passwd; thenecho "文件中找到了 'root' 用户。"
fi
if的三种结构
- 单分支
if <命令或条件>;then<要执行的代码块>
fi# 使用我们之前学过的 [[ ... ]] 作为判断条件
if [[ -d "/var/log" ]]; thenecho "/var/log 是一个目录。"
fi
- 双分支
if <命令或条件>;then<条件为真时执行的代码块>
else<条件为假时执行的代码块>
fiif [[ "$USER" == "root" ]]; thenecho "当前用户是 root,拥有最高权限。"
elseecho "当前用户是 $USER,一个普通用户。"
fi
- 多分支
if <条件1>;then<条件1为真时执行的代码块>
elif <条件2>;then<条件2为真时执行的代码块>
elif <条件3>;then<条件3为真时执行的代码块>
...
else<所有条件都为假时执行的代码块>
firead -p "请输入你的分数 (0-100): " score
if (( score >= 90 )); thenecho "优秀 (A)"
elif (( score >= 80 )); thenecho "良好 (B)"
elif (( score >= 60 )); thenecho "及格 (C)"
elseecho "不及格 (F)"
fi
if的判断条件类型
if后面可以跟任何“命令”,但最常用的是以下三种:
- 使用 test, [] 或 [[ … ]]
- 文件测试: if [[ -f “$file” ]]
- 字符串测试: if [[ “str1"=="str1" == "str1"=="str2” ]]
- 整数测试: if [[ “num1"−gt"num1" -gt "num1"−gt"num2” ]]
强烈推荐在 Bash 脚本中使用 [[ … ]],因为它更健壮、功能更强。
- 使用算术运算 ((…))
双括号 ((…)) 用于整数算术运算。它同样会返回一个退出状态码,这使得它可以非常直观地用于 if 语句中的数字比较。
count=10
if (( count > 5 )); thenecho "count 大于 5。"
fi# 注意:(( 0 )) 会被认为是 false
if (( 0 )); thenecho "这句不会被打印"
elseecho "0 在 if 中被视为 false"
fi
- 使用任意普通命令
# 检查网络连通性
if ping -c 1 -W 1 "google.com" &> /dev/null; thenecho "网络连接正常。"
elseecho "无法连接到外部网络。"
fi
# &> /dev/null 的作用是将 ping 的所有输出(标准和错误)都丢弃,我们只关心它的退出状态码。
if的一些使用注意
- 优先使用 [[…]] 和 ((…)):在编写 Bash 脚本时:进行文件和字符串判断,优先使用 [[ … ]]。进行整数判断,优先使用 (( … ))。
- 变量引用要加双引号 (“$var”):在 [[ … ]] 和特别是 [ … ] 中,对变量进行引用时,务必加上双引号。这能防止当变量值包含空格或为空时,产生意想不到的错误。
case结构
和if的高度灵活不同,case语句是根据变量的不同取值来选择分支的。
case的基本结构
case <变量> in<模式1>)<命令块1>;;<模式2>)<命令块2>;;<模式3> | <模式4>) # 模式3或模式4<命令块3>;;*)<默认命令块>;;
esac
- case <变量> in: 语句的开始。<变量> 通常是一个变量的引用,如 $action。in 是关键字。
- <模式n>): 每一个分支的匹配模式。模式后面的 ) 是必需的。
- <命令块n>: 如果变量的值匹配了该模式,则执行这里的命令。
- ;; (双分号): 这是 case 语句中至关重要的部分。它标志着一个命令块的结束,其作用类似于其他语言中 switch 的 break。执行完命令块后,遇到 ;; 就会直接跳到 esac,结束整个 case 语句。
- *): 这是一个特殊的通配符模式,可以匹配任何没有被前面模式捕获到的值。它通常作为默认分支,放在最后。
- esac: case 语句的结束标志 (case 的反写)。
case的强大之处:它天然支持通配符匹配,灵活性很强:
- *: 匹配任意长度的任意字符序列。
- ?: 匹配任意单个字符。
- […]: 匹配方括号中任意一个字符。例如 [0-9] 匹配任意数字,[a-z] 匹配任意小写字母。
- |: 或操作符,用于在一个分支中匹配多个模式
举一些例子:
read -p "请输入一个字符: " char
case "$char" in[a-z])echo "你输入了一个小写字母。";;[A-Z])echo "你输入了一个大写字母。";;[0-9])echo "你输入了一个数字。";;?)echo "你输入了一个特殊符号。";;*)echo "你输入了多个字符。";;
esac
#!/bin/bash
# 用法: ./classify_file.sh document.pdffilename="$1"case "$filename" in*.jpg | *.jpeg | *.png | *.gif)echo "'$filename' 是一个图片文件。";;*.tar.gz | *.zip | *.rar)echo "'$filename' 是一个压缩包文件。";;*.sh)echo "'$filename' 是一个 Shell 脚本文件。";;*)echo "无法识别 '$filename' 的文件类型或它没有扩展名。";;
esac
case使用的一些注意事项
- 别忘了 ;;:这是初学者最容易犯的错误。忘记写 ;; 会导致“穿透”(fall-through),即匹配成功后,脚本会继续执行下一个分支的命令块,直到遇到 ;; 或 esac 为止。这种行为在绝大多数情况下都不是我们想要的,所以请务必在每个命令块的末尾加上 ;;。
- 引用变量 (case “$var” in):在 case 语句中,总是用双引号把要测试的变量包起来。这可以防止当变量为空或包含特殊字符时出现问题。
- *) 默认分支的重要性:强烈建议总是提供一个 *) 默认分支。这能让你的脚本更健壮,可以优雅地处理所有未预料到的输入,而不是静默失败或产生错误。
- 模式的顺序:case 语句会从上到下依次匹配,一旦找到第一个匹配的模式,就会执行对应的代码块然后跳出。因此,应该将更具体的模式放在前面,更通用的模式(如 *)放在后面。
&&和||
我们知道&&和||可以在[[]]中用作逻辑判断,但如果单独使用它们也可以起到命令短路的作用。和if一样,&&和||也是根据命令执行后的状态码来工作的:
基本语法:
command1 && command2 # 只有当command1成功执行(退出状态码为0)时,command2 才会被执行。
command1 || command2 # 只有当command1执行失败(退出状态码为1)时,command2 才会被执行。
例如:
# 如果 mkdir 由于权限等问题失败,cd 就不会执行,避免了进入错误目录的风险
mkdir -p /var/data/new_project && cd /var/data/new_project# 尝试 ping 谷歌,如果失败(比如没网),就打印错误信息
ping -c 1 google.com &> /dev/null || echo "错误:网络连接中断。"
二者还可以组合起来用,实现简单的 if-then-else 逻辑:
command1 && command2 || command3# 执行逻辑:
# 执行 command1。
# 如果 command1 成功 (0),则执行 command2。
# 如果 command1 失败 (非 0),则执行 command3。
但是,这种组合不完全等价于if-else。因为它的行为依赖于 command2 的退出状态。看这个例子: true && false || echo “This gets printed”
true 执行成功。于是 && 后面的 false 被执行。false 命令执行失败(退出状态码为1)。于是 || 后面的 echo 被执行了!这通常不是我们想要的结果。在 if/else 中,如果 true 成立,else 部分是绝对不会执行的。
对于复杂的逻辑判断,使用标准的 if/then/else/fi 结构更清晰、更安全。
Bash脚本的循环结构
for循环
for循环的两种基本结构
Bash 中的 for 循环主要有两种风格:一种是传统的列表式循环,另一种是类似 C 语言的数值计算循环。
- 形式一:列表式for循环。
这是Shell脚本中最经典、最常用、最灵活的for循环形式。
for <变量名> in <项目列表>;do<循环体代码,使用 $变量名>
done
这样形式的for循环使用起来灵活多变,以下是项目列表的多种形式
# 显式列表(或数组)
for color in "red" "green" "blue"
doecho "当前颜色是: $color"
done# 范围序列(花括号扩展)
for i in {1..10..2}
doecho "奇数: $i"打印 1 3 5 7 9
done# 文件名通配符 —> 这是极其常用的方式,用于处理当前目录下的文件。
for file in *.txt
do# 引用变量时一定要加双引号,以防文件名包含空格mv "$file" "${file}.bak"echo "已将 '$file' 重命名为 '${file}.bak'"
done# 命令替换
for user in $(cat user_list.txt)
doecho "正在为用户 $user 创建家目录..."
done
# 注意:这种方式对于包含空格或特殊字符的输出处理不佳,容易出错。对于逐行读取文件内容,更健壮的方法是使用 while read 循环。# 脚本参数 ("$@")
echo "你传入了 $# 个参数,它们是:"
for arg in "$@"
doecho "参数: $arg"
done
- 形式二:C语言风格for循环。
这种形式对于有其他编程语言背景的开发者来说非常熟悉,主要用于数值计算和控制循环次数。
for (( 初始化; 条件; 迭代表达式 ));do<循环体代码>
done
举个例子:
# 计算 1 到 100 的和
sum=0
for (( i=1; i<=100; i++ ))
do(( sum += i ))
done
echo "1 到 100 的总和是: $sum"
这种写法语法直观,类似C。并且在双括号 ((…)) 内,变量引用可以不加$。
for循环的注意事项
- 引用循环变量时加双引号:这是最重要的规则之一。在循环体中使用循环变量时,请务必用双引号包裹,如 “$item”。这能防止因项目内容包含空格或特殊字符而导致命令执行失败。
- 避免 for file in $(ls) 的写法:这是一个非常经典的反面教材。ls 的输出格式不稳定,且当文件名包含空格时,$(ls) 的结果会被“分词”,导致循环出错。直接使用文件名通配符 for file in * 是更安全、更高效的做法。
while循环
while 循环用于只要某个条件持续为真(命令返回退出状态码 0),就重复执行一段代码块。
while的基本语法:
while <条件命令> ;do<循环体代码>
done
while 的条件命令和if非常像,<条件命令> 最常见的形式是 [[ … ]](测试条件)和 (( … ))(算术条件),但也可以是任何能返回退出状态码的命令。
while的常见用法
- 逐行读取文件(王牌用法)
while IFS= read -r line
doecho "正在处理行: $line"
done < "filename.txt"
说明:
- read 是一个内建命令,用于从标准输入读取一行数据。当它成功读到一行时,返回退出状态码 0;当它读到文件末尾(EOF)时,返回非 0。这恰好完美契合 while 循环的机制。
- -r: (raw read)选项,防止 read 命令对反斜杠 \进行转义。在处理文件路径或包含特殊字符的行时,强烈建议总是加上 -r。
- line: 你定义的变量名,read 命令会将读到的整行内容(除了行尾的换行符)赋值给这个变量。
- IFS 是“内部字段分隔符”的缩写,Shell 默认用它来分割单词(默认为空格、制表符、换行符)。在read命令前加上 IFS=(将其临时设置为空),可以防止 read 命令修剪掉行首行尾的空格和制表符,保证读到的 line 变量内容与文件中的行内容完全一致。这也是一个最佳实践。
- < “filename.txt”:这是输入重定向。它将 filename.txt 文件的内容作为整个 while 循环的标准输入。这样,read 命令就能从这个文件中逐行读取数据。
# servers.list文件如下
google.com
github.com
# local.server
192.168.1.1#!/bin/bash
while IFS= read -r server
do# 跳过空行和注释行if [[ -z "$server" || "$server" == \#* ]]; thencontinuefiecho "--- 正在检查服务器: $server ---"ping -c 1 "$server"
done < "servers.list"
特别注意:不能使用管道代替重定向cat file.txt | while …; do …; done。使用管道时,| 右边的 while 循环会在一个新的子 Shell中执行。这意味着在循环内部对变量做的任何修改,在循环结束后都会丢失。
- 无限循环 ->用于需要持续运行的服务、监控或主菜单程序。
# 使用 true 命令,它永远返回 0
while true
doecho "系统正在运行... 按 [CTRL+C] 退出。"# ... 执行监控任务 ...sleep 5
done
while的注意事项
- 避免死循环:确保你的循环体内部有逻辑能最终改变 while 的判断条件,使其变为假,除非你有意编写无限循环。
until循环
until循环就是while循环的反意词:until 循环会只要某个条件持续为假(命令返回非 0 退出状态码),就重复执行一段代码块。当条件第一次变为真(命令返回退出状态码 0)时,循环就会停止。
until的基本语法:
until <条件命令>;do<循环体代码>
done
其等效为:
while !<条件命令>;do<循环体代码>
done
! 是一个逻辑非操作符,它会反转后面命令的退出状态码(0 变成 1,非 0 变成 0)。
循环控制指令
和C语言一样,Bash也使用continue和break控制循环,并且功能类似。
break
基本语法:
break [n]
[n] 是一个可选的整数参数,代表要跳出的循环层数。如果省略,n 默认为 1,即跳出当前最内层的循环。
示例:
#!/bin/bash
TARGET_FILE="sshd_config"echo "开始在 /etc/ 目录中搜索文件 '$TARGET_FILE'..."for file in /etc/*
do# basename 命令用于提取路径中的文件名部分if [[ "$(basename "$file")" == "$TARGET_FILE" ]]; thenecho ""echo "找到了!文件路径是: $file"break # 任务完成,立即跳出 for 循环fi# -n 让 echo 不换行,制造一个动态的搜索效果echo -n "."sleep 0.05
doneecho ""
echo "搜索过程结束。"
continue
基本语法:
continue [n]
[n] 同样是可选参数,代表要“继续”的是第几层循环。如果省略,n 默认为 1,即作用于当前最内层的循环。
#!/bin/bash
sum=0for i in {1..10}
do# (( i % 2 != 0 )) 判断是否为奇数if (( i % 2 != 0 )); then# 如果是奇数,就跳过本次循环的剩余部分(即下面的 sum+=i)# 直接开始下一次循环(i会变成下一个值)continuefiecho "将偶数 $i 加入总和。"(( sum += i ))
doneecho "1到10之间所有偶数的和是: $sum"
注意:
- 对于 while true 或 until false 这样的无限循环,break 是唯一的程序化退出方式(不包括 exit 或 kill)。必须在循环体内设计一个或多个 if 条件来触发 break,否则循环将永不停止。
Bash脚本的函数
函数的定义
Bash 中,定义函数有两种常见的方式,它们在功能上是等价的。
函数名() {<命令块>
}# 或者function 函数名 {<命令块>
}
在脚本中,函数必须先被定义,然后才能被调用。Shell 是自上而下解释执行的,它需要先“学习”到函数的存在,之后才能执行它。
函数的调用
直接使用函数名即可:
#!/bin/bash
# 定义一个问候函数
greet() {echo "你好, 欢迎来到 Bash 函数的世界!"
}echo "准备调用函数..."
greet # 调用 greet 函数
echo "函数调用完毕。"
函数的传参
函数内部处理参数的方式与脚本处理位置参数的方式完全相同。
例如:
- $1, $2, $3…: 代表传递给函数的第 1、2、3 个参数。
- $#: 代表传递给函数的参数总数。
- $@: 代表传递给函数的所有参数列表。
# 定义一个可以向特定人问好的函数
greet_someone() {# $1 指的是传递给 greet_someone 的第一个参数echo "你好, $1! 祝你有美好的一天。"
}# 调用函数并传递参数
greet_someone "Alice"
greet_someone "Bob"
函数变量的作用域
- 局部变量
在 Bash 中,默认情况下,你在任何地方(包括函数内部)定义的变量都是全局变量。想要使用局部变量需要local关键字,local 声明的变量只在当前函数的作用域内有效,函数执行完毕后,该变量就会被销毁。
#!/bin/bash
count=100 # 全局变量update_count() {local count=10 # 这是一个全新的、只属于函数的局部变量(( count++ ))echo "函数内的 count: $count"
}echo "调用前, 全局 count: $count"
update_count
echo "调用后, 全局 count: $count" # 全局变量并未被修改# 输出
调用前, 全局 count: 100
函数内的 count: 11
调用后, 全局 count: 100
- 引用变量
默认情况下,我们输入给函数的参数在执行后不会影响原来的参数的,想要实现C语言中参数引用的效果,可以使用local -n关键字。
#/bin/bash
modify_var() {local -n ref=$1 # 引用变量名ref="新值"
}
value="旧值"
modify_var value
echo "$value" # 输出: 新值# 输出
新值 # 不加上-n输出是"旧值"
函数的执行结果
- 返回状态
return 命令用于设置函数的退出状态码 ($?),它和普通命令的退出码作用一样,用于表示函数执行的成功或失败。
is_file_exist() {if [[ -f "$1" ]]; thenreturn 0 # 文件存在,返回成功elsereturn 1 # 文件不存在,返回失败fi
}# 使用 if 来捕获函数的退出状态
if is_file_exist "/etc/hosts"; thenecho "状态检查成功:文件 /etc/hosts 存在。"
elseecho "状态检查失败:文件 /etc/hosts 不存在。"
fi
- 返回数据:使用 echo 和命令替换
Shell 函数不能像其他语言那样直接返回一个字符串、数组或其他复杂数据。
要从函数中获取数据(比如一个计算结果或处理过的字符串),标准做法是:
在函数中,使用 echo 将结果打印到标准输出;在调用函数的地方,使用命令替换 $(…) 来捕获这个输出,并赋值给一个变量。
get_timestamp() {# 将 date 命令的结果打印到标准输出echo "$(date +%Y-%m-%d_%H:%M:%S)"
}# 使用命令替换来“接收”函数的返回值
log_prefix=$(get_timestamp)
echo "[$log_prefix] 应用程序已启动。"## 输出
[2025-07-16_17:11:05] 应用程序已启动。
总结函数的注意事项
- 先定义后调用:始终确保在调用函数之前,它已经被 Shell 读取和定义。通常的做法是将所有函数定义放在脚本的开头。
- 函数内部多用 local:这是编写高质量函数的黄金法则。除非你刻意要修改一个全局状态,否则函数内的所有变量都应该用 local 声明。
- return 用于状态,echo 用于数据:清晰地区分这两种“返回”方式。不要试图用 return 来返回一个字符串(例如 return “some string” 是无效的)。
- 保持函数单一职责:一个好的函数应该只做一件事,并把它做好。这使得函数更容易理解、测试和复用。
- 传递参数使用 “@":如果需要将脚本收到的所有参数原封不动地传递给一个函数,请使用myfunction"@":如果需要将脚本收到的所有参数原封不动地传递给一个函数,请使用 my_function "@":如果需要将脚本收到的所有参数原封不动地传递给一个函数,请使用myfunction"@”。
Bash脚本的Debug
语法检查
- 使用shellcheck工具对Bash脚本进行语法检测
脚本逻辑的debug
- 注释大法,把可疑的问题代码注释一下确定问题来源
- 打印大法,对关键变量echo一下
- 把输出重定向到log文件中处理
- 借助调试命令set、trap、strace。
- set命令
set 主要用来开启“严格模式”和“追踪模式”,让脚本在出错时立即失败,或者清晰地展示出每一行代码的执行情况。可以在脚本的任何地方使用 set -<选项> 来开启,用 set +<选项> 来关闭。
- set -e :错误退出
作用:当脚本中的任何命令执行后返回非 0 的退出状态码时(即执行失败),立即退出整个脚本。这能防止错误像滚雪球一样越滚越大。例如,如果 cd到一个不存在的目录失败了,后续的 rm -rf * 就不会在错误的目录下执行,避免了灾难。
#!/bin/bash
set -eecho "准备创建一个不存在的目录..."
ls /nonexistent_directory # 这个命令会失败# 因为 set -e,下面这行代码将永远不会被执行
echo "脚本执行完毕。"
在某些情况下,set -e 不会立即退出,例如在 if 或 while 的条件判断中,或者命令后面跟着 &&, || 时,因为 Shell 认为这些错误已经被“处理”了。
- set -u:未定义变量视为错误
当脚本尝试使用一个未被定义的变量时,视为错误并立即退出。这能帮你捕捉到变量名的拼写错误。
#!/bin/bash
set -uMY_NAME="Alex"
# 假设手误,将 MY_NAME 写成了 MY_NAM
echo "你好, $MY_NAM" # 这行会触发错误,脚本退出
- set -x:执行追踪
这是最常用的调试工具。在执行每一行命令之前,Shell 会先把它打印到标准错误输出,并且是变量和通配符都已展开后的最终样子。你可以清晰地看到每个变量在执行时的实际值,以及命令最终是以何种形态被执行的。
#!/bin/bash
set -x # 开启追踪USER_COUNT=$(who | wc -l)
echo "当前有 $USER_COUNT 个用户登录。"set +x # 关闭追踪
echo "追踪已关闭。"
- set -o pipefail:管道失败
在管道命令中(如 cmd1 | cmd2),只要有任何一个命令失败,整个管道的退出状态码就是失败(非 0)。默认情况下,只有最后一个命令的退出码才算数。保证了管道中任何一步的失败都能被捕获到。
这些选项可以组合在一起使用,set -euxo pipefail 是一个非常流行的“非官方严格模式”的开头。
- trap 命令
介绍trap前需要先讲一下Linux的信号。信号是操作系统与进程之间通信的一种方式。对于 trap,最关心的几个信号是:
- EXIT: 脚本退出时触发(无论正常退出、出错退出还是被中断)。这是最适合用于清理工作的信号。
- ERR: 当任何命令执行失败(返回非 0 状态码)时触发(set -e 可能会影响其行为)。
- INT: 当用户按下 Ctrl+C 中断脚本时触发。
- TERM: 当脚本被 kill 命令(默认信号)终止时触发。
trap的语法和示例
trap '<要执行的命令或函数>' <信号1> [<信号2> ...]#!/bin/bash
# 创建一个临时文件,文件名包含进程ID以保证唯一性
TEMP_FILE="/tmp/my_app_temp_$$"
touch "$TEMP_FILE"# 定义一个清理函数
cleanup() {echo "捕获到退出信号,正在执行清理..."rm -f "$TEMP_FILE"echo "临时文件 '$TEMP_FILE' 已删除。"
}# 设置 trap:无论脚本如何退出(正常、出错、被中断),都调用 cleanup 函数
trap cleanup EXITecho "脚本正在执行,临时文件是 $TEMP_FILE"
echo "你可以尝试用 Ctrl+C 中断此脚本来测试 trap。"
sleep 20
echo "脚本正常结束。"# 当你运行这个脚本,无论你是等它自然结束,还是中途按 Ctrl+C,最后的清理函数总会被调用。
如果你想让脚本在运行时不被 Ctrl+C 中断,可以设置一个空的 trap。:
trap '' INT
echo "你无法用 Ctrl+C 中断我..."
sleep 10
注意:trap 的位置:trap 命令应该放在脚本的开头部分,以确保它能尽早生效。
- strace命令(初学者不推荐)
strace 用于追踪一个程序在运行期间发生的所有系统调用和接收到的信号。
其他
其实还有一些交互式的debug工具,由于我们写的脚本一般比较简单,这里先略过。
习题部分
题目一:
ls -l -a -h -t --color=yes
题目二:
首先说明一下我的工作目录:/home/michael/Documents/YSYX/Yuxuexi/StudyProj/2/homework
以下是我的脚本:
## macro.sh#/bin/bash
echo "$PWD" > ~/Documents/YSYX/Yuxuexi/StudyProj/2/homework/macro_dst.txt # 保存当前路径## polo.sh#!/bin/bash
cat ~/Documents/YSYX/Yuxuexi/StudyProj/2/homework/macro_dst.txt # 先显示一下保存的路径
cd "$(cat ~/Documents/YSYX/Yuxuexi/StudyProj/2/homework/macro_dst.txt)" # 载入保存的路径
在运行脚本的时候也需要注意:
export PATH="$PATH:$PWD" # 在工作目录下运行
macro.sh # 在任意目录下运行,保存路径
source polo.sh # 在任意目录下运行,并且使用source在当前shell中执行
值得说明的是答案的思路,它直接使用了source,并在marco下编写这样的脚本:
!/bin/bashmarco(){echo "$(pwd)" > $HOME/Documents/YSYX/Yuxuexi/StudyProj/2/homework/answer/marco_history.logecho "save pwd $(pwd)"
}polo(){cd "$(cat "$HOME/Documents/YSYX/Yuxuexi/StudyProj/2/homework/answer/marco_history.log")"
}
这样使用时也很方便。我的方法麻烦地多,主要是因为我之前不知道source的机制,以为source只会让脚本在当前shell执行,其实source还有其他功能: 让脚本中的变量、函数或环境变更直接影响当前 Shell 会话。
题目三
我先将题干的代码放在一个debug.sh的脚本中,然后在同一个目录下写下如下dedebug.sh的脚本:
#!/bin/bashi=0
echo "" > debug.txt #清空文件
while true; do(( i++ ))echo "***第${i}次执行***" >> debug.txt./debug.sh >> debug.txt 2>> debug.txtif [[ $? -eq 0 ]]; then #执行成功echo "现在共运行${i}次"elseecho "在第${i}次运行出现错误,现在打印报告"cat "./debug.txt"breakfi
done
题目四
find -name "*.html" -print0 | xargs -0 tar -cf html.tar
- find自带递归
- print0用来防止文件中的空格
- xargs用来讲输出的文件转成路径参数给tar
- -0是配合-print0使用的
- -cf是用来产生.tar的常用命令
上面实现的效果其实有一些不足,就是它会把包好html文件的文件夹也一起压缩,不是纯粹的提取html文件
题目五
这一题没太读懂,想着不是使用ls -R -l -t就可以按时间列出文件了吗,后来明白这一题只能输出一行,并且只能是文件(不包含文件夹),看了答案如下:
find -type f -print0 | xargs -0 ls -l -t |head -1