想象一下:你每天早上要依次做十件琐事——拉开窗帘、烧水、浇花、喂猫、取报纸。刚开始你一件件亲手做,日子久了实在烦。于是你写了一张“晨间操作单”贴在墙上,机器人按单子逐条执行,你只管喝咖啡、看晨报。Bash 脚本就是这张“操作单”,终端就是你的机器人。你把平时反复敲的命令写进一个文件,加上判断和循环规则,之后只需一条命令,机器人就按清单把所有事情办妥。这张单子还支持“如果下雨就不浇花”的条件分支、“每个房间擦一遍”的循环、把整套动作打包成“快速指令”。本篇文章就从一张空白操作单开始,把 Bash 脚本的基础语法、变量与输入、条件循环、数组字符串、函数封装、调试规范一次讲透。学完之后你会发现,运维和部署中那些繁杂的重复工作,原来可以如此优雅地一键完成。

岗位关联

对 AI 从业者来说,Bash 脚本是连接数据、模型与服务器的“万能胶水”:批量清洗数据集、拉起训练任务、监控日志告警都靠它。掌握它,你的自动化能力会立刻上一个台阶,工作效率成倍提升。

目录
  1. 可视化演示
  2. 1. 脚本基础
  3. 2. 变量与输入
  4. 3. 条件判断
  5. 4. 循环控制
  6. 5. 数组与字符串
  7. 6. 函数封装
  8. 7. 调试与严格模式
  9. 8. 实战规范
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示Bash脚本的基础语法、条件循环和函数调试三个阶段,通过步进交互理解Shell脚本自动化执行的核心流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

基础语法执行 Shebang声明+变量赋值+echo输出 命令按顺序流式执行 $符号引用变量值

1. 脚本基础

给机器人写操作手册,第一件事就是让机器人知道“这是给我看的清单,按 Bash 的规矩来”。这张清单的开头必须有一行魔法文字 #!/bin/bash,它叫 shebang,意思是“请用 Bash 这个翻译官来读后面的内容”。没有这行字,系统可能把清单当成普通文本,或者用错解释器,就像你把中文菜谱递给一个只懂英文的厨师,他会一脸茫然。

写好清单后还要“盖章生效”——用 chmod +x 脚本名 给文件加上执行权限。这相当于在手册封面盖上“准许执行”的印章。没有印章,机器人虽然能读,但不敢动手。另外,手册里用 # 开头的内容是给人类看的备注,比如“这一步是备份数据库,别乱改”,机器人会安静地跳过它们。

手册执行到最后,机器人需要向你汇报“这次任务成功了吗”。它靠的是一个数字:退出状态码。返回 0 表示一切顺利,返回非 0 表示出了岔子。你可以在上一条命令结束后立刻用 echo $? 查看这个数字,就像机器人做完事向你比一个"OK"或摇摇手。

回到操作单的比喻:shebang 是手册的“语言声明”,chmod 是“执行印章”,注释是“给同事的便签”,退出码是“完成汇报”。四者构成了每一份合格操作手册的封面和结语。从此,你不再需要手动敲一串串命令,只需执行一个文件,机器人就按手册开干。

exit_code={0successnon-zerofailure\text{exit\_code} = \begin{cases} 0 & \text{success} \\ \text{non-zero} & \text{failure} \end{cases}
Bash 约定:退出码 0 代表成功,非 0 代表失败。脚本里的每个命令都可以像这样“汇报结果”。
  1. 退出码变量$? 是 Bash 内置的特殊变量,永远保存上一条命令的退出码,像机器人额头上实时显示的表情。
    $ ?\$\ ?
  2. 成功与失败0 表示成功,非 0 表示失败;不同非 0 值还能区分不同错误原因。
    exit_code={0successnon-zerofailure\text{exit\_code} = \begin{cases} 0 & \text{success} \\ \text{non-zero} & \text{failure} \end{cases}

2. 变量与输入

操作手册里总有一些会变的值:今天浇几盆花、喂几克猫粮、报纸放在哪个邮箱。你把它们写成“便签贴”贴在墙上,机器人执行时抬头看一眼便签就行。Bash 里的变量就是这张便签。赋值时直接写 name="Alice",注意等号两边不能有空格;要用它时写 $name 或 ${name},机器人会把便签内容替换进来。

有些便签是机器人自己带来的。当你在命令行运行 ./deploy.sh prod 时,脚本内部可以通过 $1 拿到 prod,$0 是脚本自己的名字,$@ 是全部参数,$# 是参数个数。这就像你递给机器人一张写着“目标环境:生产”的纸条,它一进家门就能从口袋里掏出来看。

如果手册需要现场询问主人,可以用 read 命令让机器人停下来等你回答,比如 read -p "请输入备份目录: " dir。而 export 可以把便签贴到全家共享的公告栏上,让机器人启动的其他子程序也能看见,这就是环境变量。

变量就像操作手册里那排可以随时撕换的便签:想改目标目录、改用户名、改重试次数,都不必重写整本手册,只换便签就行。配合 $1、read、export,手册既能接收外部指令,也能当面问你问题,变得灵活又聪明。

greeting="Hello, ${name}!"\text{greeting} = \text{"Hello, } \$\{name\}\text{!"}
变量用 $ 符号触发“展开”:机器人读到 $name 时,会把便签上的内容替换进去,再拼出最终的话。
  1. 变量赋值name="Alice" 把字符串 Alice 贴到名为 name 的便签上,等号两边不能有空格。
    name="Alice"\text{name} = \text{"Alice"}
  2. 变量展开${name} 是展开语法,机器人把便签内容替换到当前位置。
    ${name}↦"Alice"\$\{name\} \mapsto \text{"Alice"}
  3. 结果拼接把展开后的内容与前后文本拼成完整问候语。
    greeting="Hello, Alice!"\text{greeting} = \text{"Hello, Alice!"}

3. 条件判断

机器人不能只会埋头执行,还得学会“看情况办”。比如操作单上写着:如果下雨,就把晾在阳台的衣服收进屋里;如果出太阳,就继续晾晒。Bash 的 if 语句就是这种判断能力。它的骨架是:if 条件; then 做一件事; else 做另一件事; fi。fi 是 if 倒过来写,表示“判断结束”。

条件怎么写?常用 test 命令或方括号 [ ]。比如 [ "$age" -ge 18 ] 判断数字 age 是否大于等于 18;字符串比较用 = 或 ==;文件判断用 -f 文件(是不是普通文件)、-d 目录(是不是目录)。需要注意方括号里面每个元素之间必须有空格,[$age -ge 18] 会直接报错,就像把“如果下雨”写成“如果下雨”缺了空格,机器人就读不懂。

还有两个偷懒但很实用的写法:&& 表示“前面的成功了才做后面的”,|| 表示“前面的失败了就做后面的”。比如 mkdir backup && echo "创建成功",一行就完成了“先建目录,成功就报告”的逻辑。

操作手册从此有了“灵性”:备份目录不存在就自动创建,日志文件超过大小就压缩,GPU 空闲才启动训练。机器人不再是愣头青,而是会看天气收衣服的贴心管家。条件判断让脚本能应对真实世界千变万化的情况。

decision={take_actionif condition_trueother_actionotherwise\text{decision} = \begin{cases} \text{take\_action} & \text{if } \text{condition\_true} \\ \text{other\_action} & \text{otherwise} \end{cases}
条件判断的本质是“布尔决策”:条件为真走一条路,为假走另一条路。Bash 用退出码 0 表示真、非 0 表示假,所以任何命令都能当条件。
  1. 条件求值Bash 执行方括号内的表达式,得到真(退出码 0)或假(退出码非 0)。
    [ condition ]↦{true,false}[\ \text{condition}\ ] \mapsto \{ \text{true}, \text{false} \}
  2. 真分支条件为真时,执行 then 后面的命令序列。
    condition_true⇒take_action\text{condition\_true} \Rightarrow \text{take\_action}
  3. 假分支条件为假时,执行 else 后的命令序列;没有 else 就什么都不做。
    condition_false⇒other_action\text{condition\_false} \Rightarrow \text{other\_action}

4. 循环控制

操作单上最烦人的一行是:“把 12 个房间的地板各擦一遍。”如果老老实实写 12 遍同样的命令,手册会又臭又长。聪明的做法是写一行:“对每个房间,执行擦地板。”Bash 的 for 循环就是这个意思:for room in 客厅 卧室 书房; do 擦地板 $room; done。机器人会挨个把元素取出来,循环执行 do 和 done 之间的命令。

除了 for,还有 while 循环:while [ 条件 ]; do ...; done,只要条件为真就反复执行,适合“没处理完就继续”的场景,比如等待某个进程退出。循环里有两个控制杆:break 是“立刻收工,后面的房间不擦了”;continue 是“这个房间跳过,下一个继续”。比如擦地板时发现杂物间上锁了,就 continue 跳过它。

循环最经典的用途是批量操作文件:for f in *.log; do gzip "$f"; done 会把当前目录下所有 .log 文件逐个压缩。一条命令,胜过手工敲几十遍。

循环是操作手册里最省力的“批量魔法”。一条 for 就能擦完所有房间、压缩所有日志、重命名所有图片。配合 break 和 continue,机器人还能在途中灵活应变,既不会死脑筋撞墙,也不会因为一个意外就撂挑子。

total_processed=∑i=1nprocessedi\text{total\_processed} = \displaystyle\sum_{i=1}^{n} \text{processed}_i
循环的本质是对一组元素逐个执行同一套动作。n 个元素就执行 n 轮,总处理量等于每一轮处理量相加。
  1. 遍历起点循环变量 i 从 1 开始,对应第一个待处理元素。
    i=1i = 1
  2. 累加处理每轮把当前处理的量累加到总数上,然后 i 加 1。
    total=total+processedi\text{total} = \text{total} + \text{processed}_i
  3. 遍历终点直到 i 超过 n 时停止,此时所有元素都处理完毕。
    i>n⇒stopi > n \Rightarrow \text{stop}

5. 数组与字符串

擦地板时,你不会把 12 个房间的名字贴在 12 张便签上到处乱贴,而是准备一个收纳盒,每个格子编上号:1 号卧室、2 号书房、3 号杂物间。Bash 的数组就是这个收纳盒。定义很简单:rooms=(客厅 卧室 书房),取第 0 个格子用 ${rooms[0]},取全部格子用 ${rooms[@]}。

更高级的是关联数组,它不用编号而用名字做标签:declare -A cfg 后,cfg[host]="192.168.1.10" 就能按名字存取配置。这就像收纳盒的每个格子上贴着“主机地址”“端口号”这样的标签,找东西一目了然。

字符串处理也是脚本里的家常便饭。${s:0:5} 表示从第 0 个字符开始切 5 个字符;${s//旧/新} 表示把所有“旧”替换成“新”;${var:-默认值} 表示变量为空时用默认值兜底。这些写法都不需要调用外部命令,速度飞快。

数组和字符串处理让操作手册从“一张纸”升级成“带分类收纳柜的工作站”。房间清单、IP 列表、配置项都能各归其位。再配合字符串切片和默认值,处理文件名、解析配置、拼接路径都变得轻松利落,不用再调用 sed、awk 这些笨重工具。

array[i]=element_at_index i\text{array}[i] = \text{element\_at\_index}\ i
数组通过下标访问元素,下标从 0 开始;关联数组则把下标换成了可读的字符串键名。
  1. 下标索引方括号里的数字 i 指明“打开第几个格子”。
    array[i]\text{array}[i]
  2. 元素取值取出的值就是数组中第 i 个元素,可以赋值给变量或直接用。
    array[i]↦elementi\text{array}[i] \mapsto \text{element}_i
  3. 整体展开${array[@]} 展开为所有元素,常配合 for 循环逐个处理。
    ${array[@]}=element1,element2,…,elementn\$\{array[@]\} = \text{element}_1, \text{element}_2, \dots, \text{element}_n

6. 函数封装

你发现机器人每天要做三件事:开窗、浇花、喂猫。与其每次写三行命令,不如录一个组合技能叫“晨间流程”,以后一个口令就触发整套动作。Bash 函数就是这个“组合技能录制”。定义方式是 morning_routine() { 开窗; 浇花; 喂猫; },调用时直接写函数名即可。

函数也能接收参数,函数内部的 $1、$2 是传给函数的参数,跟脚本的位置参数互不干扰。比如 deploy() { echo "部署到 $1"; },调用 deploy prod 就会输出“部署到 prod”。函数里用 local 声明局部变量,比如 local ts=$(date +%s),这个变量只在函数内有效,不会污染全局,就像录技能时用的草稿纸,用完就收起来。

函数结束时用 return 返回状态码,0 表示成功,非 0 表示失败。这样调用方可以用 if deploy prod; then ... 判断部署是否成功。注意函数里的 return 只退出函数本身,不会退出整个脚本;想退出整个脚本要用 exit。

函数是把操作手册变薄的利器。晨间流程、部署流程、清理流程都被录成一个个“必杀技”,主脚本看起来就像一份目录,清晰好读。更重要的是,同样的技能可以在不同脚本里反复使用,真正实现“一次录制,到处播放”。

f(x1,x2)↦result_statusf(x_1, x_2) \mapsto \text{result\_status}
函数把一组操作封装成一个带名字的单元,接收输入参数,执行内部逻辑,最后返回一个状态码作为结果。
  1. 函数定义func_name() { ... } 定义函数,函数体放在大括号内。
    f(x1,x2)=bodyf(x_1, x_2) = \text{body}
  2. 参数传入调用时传入的值在函数体内通过 $1、$2 获取。
    args↦($ 1,$ 2,… )\text{args} \mapsto (\$\ 1, \$\ 2, \dots)
  3. 返回状态return 0 表示成功,return 1 表示失败,供调用方判断。
    f(x1,x2)↦{0,1}f(x_1, x_2) \mapsto \{0, 1\}

7. 调试与严格模式

机器人也有出错的时候:浇花浇多了、擦地漏了角落。如果你不在现场,它可能一路错到底,把厨房淹了才停。Bash 给了你两样武器:安全锁和回放摄像头。安全锁就是 set -e,它让脚本在任何一个命令失败时立刻停下来,不再继续执行后面的危险操作。配合 set -u,用到没定义的变量就报错;配合 set -o pipefail,管道中任何一环失败都算整体失败。组合拳是 set -euxo pipefail,其中 -x 会逐行打印正在执行的命令,这就是回放摄像头。

调试时还可以用 bash -x script.sh 运行整个脚本,屏幕上会清楚显示每一步展开了什么、执行了什么。当脚本跑到第 30 行报错时,你回看摄像头记录,一眼就能定位是哪个变量没赋值、哪个文件路径写错了。

另一件法宝是 trap:它能在脚本收到信号或退出时执行清理动作。比如 trap 'rm -f /tmp/tmpfile' EXIT,保证即使脚本中途崩溃,临时文件也会被自动删除,像机器人摔倒前还会先把火关掉。

有了安全锁和回放摄像头,你再也不用提心吊胆地让机器人独自干活。就算出了岔子,它也会第一时间停下来向你报告,而不是带着错误一路狂奔。trap 更是给脚本上了最后一道保险,让意外退出也不留烂摊子。

execute→check_status→stop_on_error\text{execute} \rightarrow \text{check\_status} \rightarrow \text{stop\_on\_error}
严格模式的执行链:每一步执行后立刻检查状态,一旦失败就停止,而不是带着错误继续往下跑。
  1. 执行命令脚本按顺序执行每一步操作。
    execute_step⇒status\text{execute\_step} \Rightarrow \text{status}
  2. 检查状态set -e 让 Bash 自动检查上一条命令的退出码。
    status=0 ? continue:stop\text{status} = 0\ ? \ \text{continue} : \text{stop}
  3. 失败即停退出码非 0 时,脚本立即终止,避免连锁故障。
    non-zero⇒exit\text{non-zero} \Rightarrow \text{exit}

8. 实战规范

最后,一份大厨的操作手册和实习生的草稿本有什么区别?大厨的手册有固定格式:封面写菜名和用途,每步标清用量和时间,关键处有警告标记。Bash 脚本的最佳实践也一样。一个规范的脚本开头通常包括:#!/bin/bash、set -euo pipefail、脚本用途说明、用法示例、变量定义区,最后才是主逻辑。

命名规范也很重要:变量名用 snake_case 且语义清晰,比如 backup_dir 而不是 d;函数名用动词开头,比如 clean_temp_files。退出码要有约定:0 成功、1 通用错误、2 用法错误,这样调用方才能准确判断失败原因。

性能上有一个原则:尽量用 Bash 内置功能,少调用外部命令。内置的字符串处理和 for 循环比反复启动 grep、sed 快得多。批量处理一万个文件时,差距从“等几分钟”变成“眨眨眼”。

回到操作手册的比喻:规范不是束缚,而是让手册更可靠、更易维护的工程素养。一份结构清晰、命名讲究、退出码明确的脚本,三个月后你回头再看,依然能一眼读懂,就像大厨的菜谱传了三代也不会做砸。从今天开始,按规范写你的每一份 Bash “操作手册”吧。

Tbuiltin≪TexternalT_{\text{builtin}} \ll T_{\text{external}}
内置操作在 Bash 进程内直接完成,外部命令要额外启动子进程,开销大得多。所以同样功能,内置写法通常快一个数量级。
  1. 内置操作时间Bash 内置语法(变量展开、循环)几乎零额外开销。
    Tbuiltin≈negligibleT_{\text{builtin}} \approx \text{negligible}
  2. 外部命令时间每调用一次外部命令都要 fork 子进程,开销大。
    Texternal=Tfork+Texec+TioT_{\text{external}} = T_{\text{fork}} + T_{\text{exec}} + T_{\text{io}}
  3. 对比结论内置远快于外部,因此热点循环里应优先使用内置能力。
    Tbuiltin≪TexternalT_{\text{builtin}} \ll T_{\text{external}}

AI 应用场景

数据集的批量清洗与归档

阶段:数据准备

解决的问题:训练模型前,原始数据往往散落在几十个目录里,文件名混乱、格式不统一。手工一个个重命名、移动、去重,耗时且容易出错。Bash 脚本用 for 循环 + 条件判断 + 字符串处理,几分钟就能把整个数据集整理成规范目录结构。

具体案例:某算法工程师从标注平台下载了 2 万张图片,文件名带时间戳和乱码。他写了一个 Bash 脚本:按类别子目录遍历,用 ${name//[^a-z]/_} 清理非法字符,超过 500MB 的目录自动压缩归档,最后生成一份 manifest.csv 清单供训练脚本读取。

模型训练任务的一键拉起

阶段:训练运维

解决的问题:分布式训练需要在多台 GPU 服务器上同步启动、设置环境变量、检查显存、写日志。每次手动 ssh 到每台机器敲命令,效率低且容易漏步骤。Bash 脚本把整个流程封装成部署函数,一条命令拉起全部节点。

具体案例:某团队训练大模型时,使用一个 train.sh 脚本:先检查 nvidia-smi 确认显存充足,再用 for 循环 ssh 到每台工作节点启动训练进程,通过 trap 在训练异常退出时自动收集日志并通知群机器人。

训练日志监控与自动告警

阶段:模型监控

解决的问题:训练跑通只是开始,训练中途 loss 爆炸、GPU 掉卡才是噩梦。人不可能 24 小时盯着日志。通过 cron 定时任务配合 Bash 脚本,可以定期扫描日志中的 loss 值,超过阈值就发告警。

具体案例:某工程师写了一个监控脚本,用 while 循环每隔 10 分钟读取训练日志的最后一行,用 grep 提取 loss 数值,与阈值比较;异常时调用 webhook 发送企业微信告警,同时用 set -e 和 trap 保证脚本自身稳定运行。

Python 演示

python49 行
#!/usr/bin/env python3# bulk_clean.py : 批量清理超过阈值的日志文件import osimport sysimport time BACKUP_DIR = "backup"MAX_SIZE = 1024 * 1024  # 超过1MB的文件需要归档  def log(msg):    print("[%s] %s" % (time.strftime("%H:%M:%S"), msg))  def clean_file(path):    """处理单个文件,返回处理结果。"""    if os.path.getsize(path) <= MAX_SIZE:        return "skip"    target = os.path.join(BACKUP_DIR, os.path.basename(path))    if "--dry-run" in sys.argv:        return "plan"    os.rename(path, target)    return "done"  def main():    if not os.path.exists(BACKUP_DIR):        os.makedirs(BACKUP_DIR)    targets = sys.argv[1:]    if not targets:        log("usage: python3 bulk_clean.py <dir> [dir2 ...]")        return 2    files = []    for d in targets:        for name in os.listdir(d):            if name.endswith(".log"):                files.append(os.path.join(d, name))    stats = {"done": 0, "plan": 0, "skip": 0}    for path in files:        result = clean_file(path)        stats[result] += 1        log("%s -> %s" % (path, result))    log("summary: done=%d plan=%d skip=%d" % (        stats["done"], stats["plan"], stats["skip"]))    return 0  if __name__ == "__main__":    sys.exit(main())
目的:扫描指定目录下的 .log 文件,把超过 1MB 的移动到 backup 目录;带上 --dry-run 参数则只模拟不真移动,对应 Bash 脚本的调试与安全预览。
  1. if not targets:没有传入目录参数时输出用法并返回退出码 2,对应 Bash 的 usage() 习惯。
  2. for name in os.listdir(d):遍历目录下的每个文件名,对应 Bash 的 for f in *.log。
  3. if name.endswith(".log"):只收集后缀为 .log 的文件,相当于 Bash 里的文件名通配过滤。
  4. files.append(os.path.join(d, name))拼出完整路径,相当于 Bash 里 "$d/$name" 的路径拼接。
  5. result = clean_file(path)把单个文件的处理逻辑封装成函数,对应 Bash 函数封装思想。
  6. if os.path.getsize(path) <= MAX_SIZE:大小判断条件,对应 Bash 里 [ size -le MAX ] 的条件判断。
  7. if "--dry-run" in sys.argv:解析命令行参数做模拟运行,相当于 Bash 脚本中检查 $1 是否为 --dry-run。
  8. stats[result] += 1用字典统计各结果数量,对应 Bash 里用变量累加计数。
  9. sys.exit(main())把 main 的返回值作为脚本退出码,对应 Bash 的 exit 0 约定。

常见误区

误区我刚开始学 Bash 时,很自然地写下 `name = "Tom"`,心想这不是和 Python、JavaScript 一样嘛,等号两边加空格看起来更整齐,结果终端报错 `command not found`,我一脸懵。

正解Bash 对空格极度敏感。name = "Tom" 会被解析成“执行一个叫 name 的命令,参数是 = 和 Tom”。正确写法是 name="Tom",等号两侧都不能有空格。这就像写操作手册时,机器人把每个词都当成独立的指令,多一个空格就多一个词,指令就变味了。

为何常见:因为大部分现代编程语言都允许赋值语句带空格,Bash 这种“空格即分隔符”的设计对新手很反直觉。

误区我写 `for f in *.log; do rm $f; done`,心想这不挺好吗。结果文件名里有个 `my notes.log`,rm 收到两个参数 `my` 和 `notes.log`,直接报错。

正解Bash 在展开 $f 后不会自动重新分词,但展开结果如果包含空格或通配符,会被当成多个词处理。稳妥做法是始终写 "$f",让展开结果作为一个整体传给命令。好比机器人听到“擦地板卧室书房”会拆成两件事,你加上引号“擦地板卧室书房”,它就知道这是一件事的名字。

为何常见:因为在命令行交互时很少遇到带空格的文件名,写脚本时容易忽略这个差异,直到线上炸了才想起来。

误区我加了 `set -e` 后觉得万事大吉,脚本再也不会带错跑下去了。可后来某个命令明明失败了,脚本却继续执行,我怀疑 set -e 是假的。

正解set -e 不是万能的。它的失效场景包括:管道中只有最后一个命令的退出码被检查(除非加 set -o pipefail)、条件判断 if 里的命令失败不计、前面带 ! 的命令失败不计。所以严格模式要写成 set -euxo pipefail 组合拳,相当于给机器人上了全套保险而不是只锁一道门。

为何常见:set -e 的名字太有迷惑性,让人误以为它是“脚本永不犯错”的开关,而没意识到它只是一条有边界的规则。

误区我在函数里写了 `exit 1` 表示“这个环节出错了”,结果函数一调用,整个脚本直接没了,后面的清理步骤全没执行。

正解exit 是“整个脚本结束”,return 才是“退出当前函数”。函数内想表示失败应该用 return 1,把状态码交给调用方决定怎么处理。这就像机器人执行“晨间流程”时,浇花环节发现花洒坏了,它应该停下这个环节向主人报告,而不是直接把整栋房子的电闸拉了。

为何常见:exit 和 return 在别的语言里语义差别很大,新手容易把“退出当前逻辑”和“终止整个程序”混为一谈。

误区我写 `[ "$num" == "10" ]` 判断数字大小,结果 num=9 时它却说成立,我完全想不通。

正解== 和 = 比较的是字符串内容,不是数值大小。"9" == "10" 在字符串层面当然不相等,但如果你比较 "10" == "10" 那只是巧合。数值比较要用 -eq(等于)、-ne(不等)、-gt(大于)、-lt(小于)。这就像机器人分不清“编号 9”和“数值 10 谁大”,你让它比大小得用尺子(-gt),而不是比标签文字(==)。

为何常见:几乎所有现代语言里 == 都能做数值比较,Bash 却把字符串和数值比较分成了两套运算符,这是 Shell 历史设计留下的坑。

面试题

Q1. #!/bin/bash 这一行在脚本中的作用是什么?

单选题 · easy

  • A.声明脚本文件的编码格式
  • B.指定解释器执行脚本← 正确答案
  • C.给脚本添加执行权限
  • D.输出脚本运行的日志

答案:B。shebang 行告诉操作系统用哪个解释器来执行这个脚本。编码格式由文件本身决定,执行权限靠 chmod,日志靠 echo,所以只有 B 正确。这就像在操作手册封面写明“请用 Bash 翻译官阅读本手册”。

Q2. 执行 chmod +x run.sh 后,会带来什么变化?

单选题 · easy

  • A.脚本运行速度变得更快
  • B.脚本文件内容会被加密
  • C.脚本获得可执行权限← 正确答案
  • D.脚本自动修复语法错误

答案:C。chmod +x 是给文件加上“可执行”权限,相当于在操作手册上盖了“准许执行”的章。它不改变内容、不加快速度、也不修 bug,只是让系统允许直接运行这个文件。

Q3. 下面哪条是 Bash 变量赋值的正确写法?

单选题 · easy

  • A.name = "Tom"
  • B.name="Tom"← 正确答案
  • C.$name="Tom"
  • D.name<-"Tom"

答案:B。Bash 变量赋值要求等号两侧不能有空格,所以 B 正确。A 会被解析成执行 name 命令,C 把 $ 放错了位置,D 的箭头写法在 Bash 里不是赋值语法。记住:便签贴要紧紧贴在盒子上,不能留缝隙。

Q4. 在脚本中执行 echo $? 会立刻输出什么信息?

单选题 · mid

  • A.当前脚本执行的行数
  • B.上条命令的退出状态码← 正确答案
  • C.当前用户的用户编号
  • D.上条命令的最终输出

答案:B。$? 是 Bash 的特殊变量,保存上一条命令的退出码:0 成功、非 0 失败。它不是行号、不是用户 ID、也不是命令输出。就像机器人额头上的表情,实时反映刚才那件事办得怎么样。

Q5. 判断变量 a 的数值是否大于 b,下面哪个写法正确?

单选题 · mid

  • A.[ $a > $b ]
  • B.[ $a -gt $b ]← 正确答案
  • C.[ $a == $b ]
  • D.if $a > $b

答案:B。数值比较必须用 -gt(大于)、-lt(小于)、-eq(等于)等运算符,B 正确。A 里的 > 在方括号里会被当成输出重定向,C 的 == 是字符串比较,D 的 if 语法根本不对。数值比大小要用尺子,不能用比标签的方式。

Q6. 执行 for f in *.txt; do echo "$f"; done 会输出什么?

单选题 · mid

  • A.把所有文件名打印成一行
  • B.逐个输出每个 txt 文件名← 正确答案
  • C.只输出第一个 txt 文件名
  • D.统计 txt 文件的总数量

答案:B。for 循环把当前目录下匹配 *.txt 的每个文件名依次赋给 f,然后分别执行 echo,所以是逐个分行输出。就像机器人挨个房间擦地板,每个房间单独报告。它不统计数量,也不合并打印。

Q7. set -e 在 Bash 脚本中的核心作用是什么?

单选题 · mid

  • A.每次运行结束后自动退出终端
  • B.某条命令失败时立即停止脚本← 正确答案
  • C.提升脚本整体的执行效率
  • D.隐藏脚本产生的错误输出

答案:B。set -e 让脚本在任意命令返回非 0 退出码时立刻终止,避免带着错误继续执行。它不退出终端、不加速、不屏蔽输出。相当于给机器人装了“出错即停”的安全锁,防止厨房被淹了还在继续浇花。

Q8. 关于 Bash 函数中的 return 和脚本的 exit,下列说法正确的有哪些?

多选题 · mid

  • A.return 只退出当前函数← 正确答案
  • B.exit 会结束整个脚本进程← 正确答案
  • C.return 和 exit 可以互换使用
  • D.return 可以携带状态码返回← 正确答案

答案:ABD。return 退出当前函数并返回状态码,供调用方判断;exit 终止整个脚本进程,一般用于脚本收尾。两者作用范围完全不同,不能互换,所以 C 错误。就像机器人录制的“组合技能”中途停下与整个机器人关机断电是两码事。

Q9. 关于 set -euxo pipefail 这一组合,下列说法正确的有哪些?

多选题 · hard

  • A.-e 让脚本在出错时立即停止← 正确答案
  • B.-x 会逐行打印执行的命令← 正确答案
  • C.-u 使用未定义变量就报错← 正确答案
  • D.pipefail 只看管道最后一个命令

答案:ABC。-e 出错即停,-x 打印执行痕迹,-u 杜绝未定义变量,这些都是正确描述。pipefail 恰恰相反:它让管道中任何一个命令失败都导致整体失败,而不是只看最后一条,所以 D 错误。这四件套组合起来才是完整的“安全锁 + 回放摄像头”。

Q10. 关于脚本中的 trap 命令,下列说法正确的有哪些?

多选题 · extreme

  • A.可以在脚本退出前执行清理动作← 正确答案
  • B.可以捕获并忽略 SIGKILL 信号
  • C.常用于删除临时文件和回收资源← 正确答案
  • D.可以捕获 Ctrl-C 中断信号← 正确答案

答案:ACD。trap 可以在收到信号或 EXIT 时执行清理,比如删除临时文件、关闭连接,A 和 C 正确。Ctrl-C 对应 SIGINT 信号,也可以被 trap 捕获,D 正确。但 SIGKILL(强杀)无法被任何进程捕获处理,B 错误。trap 就像机器人摔倒前先伸手关掉煤气灶的安全网。