大厂面试官最爱问的 20 个 Shell 难题,答不上来基本就挂了
前言
Shell 脚本是 Linux/Unix 系统自动化任务的核心工具,也是运维、DevOps、后端开发、运维岗位面试中的高频考察点。然而,很多人在面试时发现,自己平时写脚本还挺溜,一遇到面试官的连环追问就卡壳了——原因很简单:面试官考的不是“能不能写出来”,而是“你有没有踩过坑”。
本文整理了 20 道大厂面试中真实的 Shell 高频难题,每一题都配有陷阱解析、输出示例和最佳实践。建议先遮住答案自己想一遍,再看解析——效果翻倍。
🎯 阅读提示:文中所有脚本都可以在你自己的 Linux/Mac 终端中运行验证。建议边看边敲,印象更深。
🔧 准备一个“模拟生产环境”
在开始之前,我们先搭建一个测试目录,方便后续脚本执行时有真实数据可操作。在终端中依次执行以下命令:
# 创建测试目录
mkdir -p ~/shell-interview-test
cd ~/shell-interview-test
# 创建带权限和特殊字符的文件
touch normal_file.txt
echo "hello world" > "file with spaces.txt"
echo "line1" > "file_with_backslash\name.txt"
mkdir subdir
touch subdir/.hidden_file
chmod 000 no_permission_file.txt 2>/dev/null || touch no_permission_file.txt && chmod 000 no_permission_file.txt
# 查看创建结果
ls -la
输出示例:
total 8
drwxr-xr-x 3 user staff 96 May 21 10:00 .
drwxr-xr-x 5 user staff 160 May 21 10:00 ..
-rw-r--r-- 1 user staff 12 May 21 10:00 file with spaces.txt
---------- 1 user staff 0 May 21 10:00 no_permission_file.txt
-rw-r--r-- 1 user staff 0 May 21 10:00 normal_file.txt
drwxr-xr-x 3 user staff 96 May 21 10:00 subdir
这个目录包含了带空格的文件名、无权限文件、隐藏目录等“坑”,后面很多题目都会用到它。
基础陷阱题(1-5 题)
第 1 题:变量赋值为什么报错?
面试官:“请指出下面脚本的错误,并修正。”
#!/bin/bash
name = "Alice"
echo $name
你的答案是什么?思考 10 秒再看解析。
点击查看陷阱解析
陷阱:Shell 中变量赋值的等号两边绝对不能有空格,这是新手最高频的错误。
# ❌ 错误:等号两边有空格
name = "Alice" # Shell 会把 name 当成命令去执行
# ✅ 正确
name="Alice"
echo "$name"
执行结果对比:
# 错误版本
$ name = "Alice"
bash: name: command not found
# 正确版本
$ name="Alice"
$ echo "$name"
Alice
最佳实践:引用变量时始终加双引号"$var",防止变量值为空或包含空格时出现意外行为-29。另外,推荐使用 ${var} 而非 $var,在拼接字符串时更安全-5。
第 2 题:[ ] 和 [[ ]] 到底有什么区别?
面试官:“你在条件判断中用单括号还是双括号?为什么?”
#!/bin/bash
var="hello world"
if [ $var == "hello world" ]; then
echo "Match"
fi
这段代码会报错,为什么?
点击查看陷阱解析
陷阱:[ ] 是 POSIX 标准的 test 命令,会对变量进行单词拆分。$var 的值 "hello world" 被拆分成了 hello 和 world 两个参数,导致语法错误。
# ❌ 错误
$ var="hello world"
$ if [ $var == "hello world" ]; then echo "Match"; fi
bash: [: too many arguments
# ✅ 正确:双引号包裹变量
$ if [ "$var" == "hello world" ]; then echo "Match"; fi
Match
# ✅ 更推荐:使用 [[ ]](bash 内置,不进行单词拆分)
$ if [[ $var == "hello world" ]]; then echo "Match"; fi
Match
[ ] vs [[ ]] 核心区别:
💡 最佳实践:写 bash 脚本时优先使用 [[ ]],代码更安全、更简洁-5。
第 3 题:$@ 和 $* 的细微差别,99% 的人答不全
面试官:“$@ 和 $* 有什么区别?什么时候用哪个?”
这是一道经典的“区分度极高”的面试题。
点击查看陷阱解析
先看测试脚本:
#!/bin/bash
# test_args.sh
echo "=== 不加引号 ==="
echo '$@:'
for arg in $@; do echo " [$arg]"; done
echo '$*:'
for arg in $*; do echo " [$arg]"; done
echo ""
echo "=== 加双引号 ==="
echo '"$@":'
for arg in "$@"; do echo " [$arg]"; done
echo '"$*":'
for arg in "$*"; do echo " [$arg]"; done
$ ./test_args.sh "arg one" "arg two" "arg three"
=== 不加引号 ===
$@:
[arg]
[one]
[arg]
[two]
[arg]
[three]
$*:
[arg]
[one]
[arg]
[two]
[arg]
[three]
=== 加双引号 ===
"$@":
[arg one]
[arg two]
[arg three]
"$*":
[arg one arg two arg three]
关键区别:
💡 最佳实践:处理脚本参数时,始终使用 "$@",它能正确保留每个参数中的空格。
第 4 题:source 和 ./script.sh 有什么区别?
面试官:“source script.sh 和 ./script.sh 有什么区别?什么场景下必须用 source?”
点击查看陷阱解析
# 准备一个脚本
cat > test_env.sh << 'EOF'
#!/bin/bash
export MY_VAR="hello from script"
echo "Script: MY_VAR=$MY_VAR"
EOF
chmod +x test_env.sh
# 方式一:直接执行
$ ./test_env.sh
Script: MY_VAR=hello from script
$ echo $MY_VAR
# 空!变量没有留在当前 Shell
# 方式二:source 执行
$ source test_env.sh
Script: MY_VAR=hello from script
$ echo $MY_VAR
hello from script # 变量保留在当前 Shell!
核心区别:
💡 最佳实践:需要修改当前 Shell 环境(如激活虚拟环境、加载配置文件)时用 source-1;执行独立任务用 ./script.sh。
第 5 题:exit 和 return 用错会有什么后果?
面试官:“在函数中用 exit 会发生什么?为什么?”
点击查看陷阱解析
#!/bin/bash
# 测试 exit vs return
function test_exit() {
echo "进入函数"
exit 1 # 🚨 直接终止整个脚本进程
echo "这行不会执行"
}
function test_return() {
echo "进入函数"
return 1 # ✅ 仅退出函数,返回状态码
echo "这行也不会执行"
}
echo "=== 测试 return ==="
test_return
echo "return 退出码: $?"
echo "脚本继续执行!"
echo ""
echo "=== 测试 exit ==="
test_exit
echo "这行永远不会执行"
执行结果:
=== 测试 return ===
进入函数
return 退出码: 1
脚本继续执行!
=== 测试 exit ===
进入函数
脚本在 test_exit 中直接终止了,后面的内容全部不执行。
💡 最佳实践:
-
函数内用
return N,返回状态码 -
脚本顶层或需要终止整个脚本时用
exit N -
约定:
0表示成功,非0表示失败-3
字符串与文本处理(6-9 题)
第 6 题:如何安全地逐行读取文件?
面试官:“写一个脚本,逐行读取一个日志文件并处理每一行。你有什么要注意的?”
点击查看陷阱解析
这是大厂面试中最高频的实操题之一,也是生产环境中最容易写出 bug 的地方。
先准备测试文件:
# 创建一个包含各种“坑”的测试文件
cat > test_log.txt << 'EOF'
第一行正常内容
第二行 有 多余 空格
第三行前后有空格
第四行末尾有反斜杠\
第五行有$变量符号
第六行
第七行有制表符 在这里
EOF
现在看几种写法:
#!/bin/bash
echo "=== 方式一:for 循环(❌ 错误) ==="
for line in $(cat test_log.txt); do
echo "[$line]"
done
# 问题:单词拆分、通配符展开、空行丢失
$ bash read_test1.sh
=== 方式一:for 循环(❌ 错误) ===
[第一行正常内容]
[第二行]
[有]
[多余]
[空格]
[第三行前后有空格]
[第四行末尾有反斜杠\]
[第五行有]
... # 完全乱套了
#!/bin/bash
echo "=== 方式二:while read(✅ 正确) ==="
while IFS= read -r line; do
echo "[$line]"
done < test_log.txt
$ bash read_test2.sh
=== 方式二:while read(✅ 正确) ===
[第一行正常内容]
[第二行 有 多余 空格]
[ 第三行前后有空格 ]
[第四行末尾有反斜杠\]
[第五行有$变量符号]
[第六行]
[第七行有制表符 在这里]
关键参数解释:
💡 最佳实践:逐行读取文件务必使用while IFS= read -r line; do ... done < file,这是社区公认的最安全写法-39。
第 7 题:统计日志中每个 IP 的访问次数
面试官:“一个 access.log 文件有几十万行,每行格式为 IP - - [时间] "请求" 状态码 大小,请统计访问次数最多的 Top 5 IP。”
这道题字节跳动和美团都考过。
点击查看陷阱解析
先准备测试数据:
cat > access.log << 'EOF'
192.168.1.1 - - [21/May/2026:10:00:00] "GET /index.html" 200 1024
10.0.0.5 - - [21/May/2026:10:00:01] "POST /api/login" 200 512
192.168.1.1 - - [21/May/2026:10:00:02] "GET /style.css" 200 2048
172.16.0.8 - - [21/May/2026:10:00:03] "GET /index.html" 304 0
10.0.0.5 - - [21/May/2026:10:00:04] "GET /api/data" 500 128
192.168.1.1 - - [21/May/2026:10:00:05] "GET /favicon.ico" 404 64
10.0.0.5 - - [21/May/2026:10:00:06] "GET /api/status" 200 256
172.16.0.8 - - [21/May/2026:10:00:07] "POST /api/login" 401 96
10.0.0.5 - - [21/May/2026:10:00:08] "GET /api/data" 200 512
192.168.1.2 - - [21/May/2026:10:00:09] "GET /index.html" 200 1024
EOF
$ awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -5
4 10.0.0.5
3 192.168.1.1
2 172.16.0.8
1 192.168.1.2
如果想更完整地展示:
$ awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -5 | \
awk '{printf "%-15s %s 次\n", $2, $1}'
10.0.0.5 4 次
192.168.1.1 3 次
172.16.0.8 2 次
192.168.1.2 1 次
命令拆解:
💡 最佳实践:大数据量场景优先用 awk 流式处理,避免 Shell 循环逐行处理带来的性能问题-39。
加分回答:面试官可能会追问“如果日志文件有 10GB 怎么办?”——答:仍然用
awk,流式处理内存占用恒定,比把文件加载到变量中高效得多-39。
第 8 题:批量重命名文件——for 循环中的坑
面试官:“当前目录下有 file1.txt、file2.txt ... file10.txt,请写脚本批量添加 backup_ 前缀。”
点击查看陷阱解析
先创建测试文件:
touch file1.txt file2.txt file3.txt "file with spaces.txt" "file*.txt"
ls
$ ls
file1.txt file2.txt file3.txt
file with spaces.txt file*.txt
很多人的第一反应:
#!/bin/bash
# ❌ 有坑的写法
for f in $(ls *.txt); do
mv "$f" "backup_$f"
done
$ bash rename_bad.sh
mv: rename file to backup_file: No such file or directory
mv: rename with to backup_with: No such file or directory
... # 带空格的文件名被拆分了
# file*.txt 这个文件也会被通配符展开导致问题
正确写法:
#!/bin/bash
# ✅ 使用通配符直接遍历
for f in *.txt; do
[[ -e "$f" ]] || continue # 防止没有匹配文件时 f 变成字面量 "*.txt"
mv "$f" "backup_$f"
echo "重命名: $f -> backup_$f"
done
$ bash rename_good.sh
重命名: file1.txt -> backup_file1.txt
重命名: file2.txt -> backup_file2.txt
重命名: file3.txt -> backup_file3.txt
重命名: file with spaces.txt -> backup_file with spaces.txt
重命名: file*.txt -> backup_file*.txt
$ ls backup_*
backup_file*.txt backup_file1.txt backup_file2.txt
backup_file3.txt backup_file with spaces.txt
🚨 陷阱总结:
第 9 题:sed 和 awk 的核心区别?什么时候用哪个?
面试官:“请说说 sed 和 awk 的本质区别。举个例子。”
点击查看陷阱解析
用一句话总结:
sed是“流编辑器”,强在行的增删改查;awk是“文本处理语言”,强在字段级的计算和格式化。
# 准备数据
cat > data.txt << 'EOF'
张三 85 92 78
李四 90 88 95
王五 76 82 70
EOF
sed 擅长的事——行的增删改:
# 删除第2行
$ sed '2d' data.txt
张三 85 92 78
王五 76 82 70
# 替换文本
$ sed 's/张三/张老三/' data.txt
张老三 85 92 78
李四 90 88 95
王五 76 82 70
awk 擅长的事——字段计算:
# 计算每个人的总分和平均分
$ awk '{total=$2+$3+$4; avg=total/3; printf "%-6s 总分:%3d 平均:%.1f\n", $1, total, avg}' data.txt
张三 总分:255 平均:85.0
李四 总分:273 平均:91.0
王五 总分:228 平均:76.0
选择指南:
进阶实战(10-14 题)
第 10 题:管道中的变量作用域问题
面试官:“下面这个脚本,为什么 count 最后是 0?”
#!/bin/bash
count=0
cat /etc/passwd | while read line; do
count=$((count + 1))
done
echo "总行数: $count"
点击查看陷阱解析
$ bash pipe_scope_test.sh
总行数: 0
为什么是 0? 管道 | 右侧的 while 循环运行在子 Shell 中,子 Shell 中对 count 的修改不会影响父 Shell-39。
三种解决方案:
#!/bin/bash
# 方案一:进程替换(推荐)
count=0
while read line; do
count=$((count + 1))
done < <(cat /etc/passwd)
echo "方案一: $count"
# 方案二:重定向(最简洁)
count=0
while read line; do
count=$((count + 1))
done < /etc/passwd
echo "方案二: $count"
# 方案三:命名管道(不推荐,仅作了解)
$ bash pipe_fix.sh
方案一: 38
方案二: 38
💡 最佳实践:需要修改变量时,用重定向或进程替换代替管道。
第 11 题:set -e 的“暗坑”——你踩过吗?
面试官:“你用过 set -e 吗?它有什么隐藏的坑?”
很多面试者信心满满地说 set -e 会让脚本在任何错误时退出,但这个回答不够。
点击查看陷阱解析
#!/bin/bash
set -e
# 场景一:管道中的错误会被忽略!
false | true
echo "场景一:这行执行了!(管道默认只看最后一个命令)"
# 场景二:条件判断中的错误不会退出
if grep "不存在的模式" /etc/passwd; then
echo "找到了"
else
echo "没找到(grep 返回非零但脚本没退出)"
fi
# 场景三:let 和 ((...)) 中的零值会导致退出
x=0
# ((x++)) # 如果取消注释,x++ 在 x=0 时返回 1(false),脚本直接退出!
echo "场景三:这行不会执行如果上面取消注释"
set -e 失效的场景:
💡 最佳实践:
#!/bin/bash
# 严谨的脚本开头
set -euo pipefail
# -e: 命令失败时退出
# -u: 使用未定义变量时报错
# -o pipefail: 管道中任何命令失败都视为管道失败
# 配合 trap 做清理
trap 'echo "脚本异常退出,行号: $LINENO"' ERR
第 12 题:如何用 Shell 实现并发控制?
面试官:“有 100 个任务需要并发执行,但最多同时运行 10 个。怎么写?”
这是大厂 SRE 岗位的常见场景题-41。
点击查看陷阱解析
#!/bin/bash
# 并发控制:最多同时运行 3 个任务
MAX_JOBS=3
running=0
task() {
local id=$1
echo "[$(date +%H:%M:%S)] 任务 $id 开始"
sleep $((RANDOM % 5 + 1)) # 模拟耗时任务
echo "[$(date +%H:%M:%S)] 任务 $id 完成"
}
for i in {1..10}; do
task "$i" & # 后台运行
running=$((running + 1))
if [[ $running -ge $MAX_JOBS ]]; then
wait -n # 等待任意一个任务完成
running=$((running - 1))
fi
done
wait # 等待所有剩余任务完成
echo "所有任务完成!"
$ bash parallel_test.sh
[10:30:01] 任务 1 开始
[10:30:01] 任务 2 开始
[10:30:01] 任务 3 开始
[10:30:04] 任务 2 完成
[10:30:04] 任务 4 开始
[10:30:05] 任务 3 完成
[10:30:05] 任务 5 开始
...
所有任务完成!
关键机制:
进阶方案(生产环境推荐):
# 使用 xargs 的 -P 参数更简洁
seq 1 10 | xargs -P 3 -I {} bash -c 'echo "任务 {} 开始"; sleep $((RANDOM % 5 + 1)); echo "任务 {} 完成"'
$ seq 1 10 | xargs -P 3 -I {} bash -c 'echo "任务 {} 开始"; sleep 3; echo "任务 {} 完成"'
任务 1 开始
任务 2 开始
任务 3 开始
任务 1 完成
任务 4 开始
... # 始终保持 3 个并发
第 13 题:脚本中如何优雅地处理信号和清理工作?
面试官:“你的脚本在运行中突然被 Ctrl+C 中断了,怎么确保临时文件被清理?”
点击查看陷阱解析
#!/bin/bash
# 信号处理与优雅退出
# 创建临时文件
TEMP_FILE=$(mktemp /tmp/myscript.XXXXXX)
TEMP_DIR=$(mktemp -d /tmp/myscript_dir.XXXXXX)
echo "临时文件: $TEMP_FILE"
echo "临时目录: $TEMP_DIR"
echo "PID: $$"
# 清理函数
cleanup() {
local exit_code=$?
echo ""
echo ">>> 正在清理..."
rm -f "$TEMP_FILE"
rm -rf "$TEMP_DIR"
echo ">>> 清理完成"
exit $exit_code
}
# 捕获信号
trap cleanup EXIT SIGINT SIGTERM
# 模拟工作
echo "开始工作...(按 Ctrl+C 中断测试)"
for i in {1..10}; do
echo " 处理 $i/10..."
sleep 1
done
echo "工作完成!"
$ bash trap_demo.sh
临时文件: /tmp/myscript.aB3Xk9
临时目录: /tmp/myscript_dir.cD4Yz1
PID: 12345
开始工作...(按 Ctrl+C 中断测试)
处理 1/10...
处理 2/10...
^C # 用户按下 Ctrl+C
>>> 正在清理...
>>> 清理完成
trap 捕捉的常用信号:
💡 最佳实践:任何创建临时文件的脚本,都应当用 trap cleanup EXIT 确保清理逻辑一定执行。
第 14 题:大文件处理——为什么我的脚本内存爆了?
面试官:“你用 Shell 处理过一个 2GB 的日志文件吗?如果脚本内存占用超过 5GB,可能是什么原因?”
点击查看陷阱解析
先直观对比两种写法:
#!/bin/bash
# 准备测试数据(模拟 100MB 的日志文件)
yes "192.168.1.1 - - [21/May/2026:10:00:00] \"GET /api/data HTTP/1.1\" 200 1024" | head -n 500000 > big_log.txt
ls -lh big_log.txt
$ ls -lh big_log.txt
-rw-r--r-- 1 user staff 57M May 21 10:30 big_log.txt
❌ 错误写法——内存爆炸:
#!/bin/bash
# 🚨 将整个文件内容加载到变量中
content=$(cat big_log.txt)
for line in $content; do
echo "$line" | grep "200" > /dev/null
done
echo "处理完成"
监控这段脚本的内存:
$ /usr/bin/time -v bash bad_memory.sh 2>&1 | grep "Maximum resident"
Maximum resident set size (kbytes): 61024 # 约 60MB(文件本身大小)
如果文件是 2GB,这里的内存占用就是 2GB+!
✅ 正确写法——恒定内存:
#!/bin/bash
# ✅ 流式处理,内存占用恒定
awk '/200/ {print $0}' big_log.txt > /dev/null
echo "处理完成"
$ /usr/bin/time -v bash good_memory.sh 2>&1 | grep "Maximum resident"
Maximum resident set size (kbytes): 1832 # 约 1.8MB,几乎不随文件大小增长
💡 核心原则:
高阶陷阱(15-17 题)
第 15 题:rm -rf 的灾难性拼写错误
面试官:“你如何避免 Shell 脚本中的危险操作?比如 rm -rf 意外删库?”
这是一道考察安全意识的问题。
点击查看陷阱解析
#!/bin/bash
# 场景模拟:变量为空导致灾难
APP_DIR="" # 假设这个变量应该是 "/opt/myapp"
echo "准备清理: $APP_DIR"
# 🚨 如果 APP_DIR 为空,下面这行等价于 rm -rf / !!!
# rm -rf "$APP_DIR"/* # 千万不要运行这行!
# ✅ 安全做法:先检查变量是否为空
cleanup_dir() {
local dir="$1"
# 安全检查
if [[ -z "$dir" ]]; then
echo "错误: 目录变量为空!" >&2
return 1
fi
if [[ ! -d "$dir" ]]; then
echo "错误: $dir 不是目录!" >&2
return 1
fi
# 额外保护:禁止删除根目录和家目录
case "$(realpath "$dir")" in
/|/root|/home)
echo "严重错误: 拒绝删除系统关键目录!" >&2
return 1
;;
esac
echo "安全清理: $dir"
# rm -rf "$dir"/*
}
cleanup_dir "$APP_DIR"
$ bash safe_rm.sh
准备清理:
错误: 目录变量为空!
历史教训:著名的 rm -rf $PREFIX/* 事故——当 $PREFIX 为空时,命令变成了 rm -rf /*,直接删除了整个系统。
💡 防御性编程 Checklist:
第 16 题:${var} 的参数扩展——比你以为的强大得多
面试官:“请解释以下写法的含义:${var:-default}、${var:=value}、${var:?error}、${var:+alt}、${var#prefix}、${var%suffix}”
这道题能区分“用过 Shell”和“精通 Shell”的候选人。
点击查看陷阱解析
#!/bin/bash
echo "=== 参数扩展完整演示 ==="
echo ""
# 1. ${var:-word} — 如果 var 未设置或为空,使用默认值(不改变原变量)
echo "--- \${var:-default} ---"
unset MY_VAR
echo "${MY_VAR:-默认值}" # 输出: 默认值
echo "原变量: [$MY_VAR]" # 输出: [] (仍然未设置)
# 2. ${var:=word} — 同上,但会赋值给原变量
echo "--- \${var:=default} ---"
unset MY_VAR
echo "${MY_VAR:=新值}" # 输出: 新值
echo "原变量: [$MY_VAR]" # 输出: [新值] (已被赋值)
# 3. ${var:?error} — 如果未设置,打印错误并退出
echo "--- \${var:?error} ---"
unset MY_VAR
# echo "${MY_VAR:?变量未设置!}" # 取消注释会导致脚本退出
# 4. ${var:+alt} — 如果已设置,使用替代值
echo "--- \${var:+alt} ---"
MY_VAR="已设置"
echo "${MY_VAR:+存在!}" # 输出: 存在!
# 5. 字符串截取
echo "--- 字符串截取 ---"
FILE="backup_2026-05-21.tar.gz"
echo "原文件名: $FILE"
echo "去掉前缀 backup_: ${FILE#backup_}"
echo "去掉后缀 .tar.gz: ${FILE%.tar.gz}"
echo "贪婪去掉前缀: ${FILE##*_}" # 从前往后,最长匹配
echo "贪婪去掉后缀: ${FILE%%.*}" # 从后往前,最长匹配
$ bash param_expansion.sh
=== 参数扩展完整演示 ===
--- ${var:-default} ---
默认值
原变量: []
--- ${var:=default} ---
新值
原变量: [新值]
--- ${var:?error} ---
--- ${var:+alt} ---
存在!
--- 字符串截取 ---
原文件名: backup_2026-05-21.tar.gz
去掉前缀 backup_: 2026-05-21.tar.gz
去掉后缀 .tar.gz: backup_2026-05-21
贪婪去掉前缀: 21.tar.gz
贪婪去掉后缀: backup_2026-05-21
速查表:
第 17 题:Shell 中的“伪信号”——trap 高级用法
面试官:“你知道 Shell 的 DEBUG 和 ERR 伪信号吗?它们能用来做什么?”
点击查看陷阱解析
#!/bin/bash
# Shell 伪信号:DEBUG、ERR、EXIT
echo "=== DEBUG 伪信号:每条命令执行前触发 ==="
trap 'echo ">>> 即将执行行号 $LINENO"' DEBUG
x=10
y=20
z=$((x + y))
echo "结果: $z"
trap - DEBUG # 取消 DEBUG trap
echo ""
echo "=== ERR 伪信号:命令失败时触发 ==="
trap 'echo "!!! 错误发生在行号 $LINENO,退出码: $?"' ERR
ls /nonexistent_dir # 这会触发 ERR
echo "这行仍会执行(ERR 不会退出脚本)"
trap - ERR
echo ""
echo "=== 组合使用:完整的错误追踪 ==="
trap 'echo "[ERR] 行号 $LINENO 出错,退出码 $?"' ERR
trap 'echo "[EXIT] 脚本退出"' EXIT
trap 'echo "[DEBUG] 行号 $LINENO"' DEBUG
false # 触发 ERR,但不退出
echo "脚本继续运行"
$ bash pseudo_signals.sh
=== DEBUG 伪信号:每条命令执行前触发 ===
>>> 即将执行行号 8
>>> 即将执行行号 9
>>> 即将执行行号 10
结果: 30
>>> 即将执行行号 12
=== ERR 伪信号:命令失败时触发 ===
ls: /nonexistent_dir: No such file or directory
!!! 错误发生在行号 17,退出码: 2
这行仍会执行(ERR 不会退出脚本)
=== 组合使用:完整的错误追踪 ===
[DEBUG] 行号 24
[DEBUG] 行号 25
[DEBUG] 行号 26
[ERR] 行号 26 出错,退出码 1
[DEBUG] 行号 27
脚本继续运行
[EXIT] 脚本退出
伪信号用途:
大厂真题实战(18-20 题)
第 18 题:实现一个带重试机制的 HTTP 健康检查脚本
真题来源:阿里巴巴运维岗面试题
题目:编写一个 Shell 脚本,对指定的 URL 进行健康检查。要求:
-
支持自定义重试次数和间隔
-
超时时间可配置
-
返回明确的状态码(0=正常, 1=异常)
-
打印详细的检查日志
点击查看解析
#!/bin/bash
# health_check.sh — 带重试机制的健康检查
# 使用方式: ./health_check.sh <URL> [重试次数] [间隔秒数] [超时秒数]
set -euo pipefail
# ==================== 配置 ====================
URL="${1:-http://localhost:8080/health}"
MAX_RETRIES="${2:-3}"
RETRY_INTERVAL="${3:-2}"
TIMEOUT="${4:-5}"
# 日志函数
log_info() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*"; }
log_warn() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] $*" >&2; }
log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2; }
# ==================== 主逻辑 ====================
log_info "开始健康检查: $URL"
log_info "参数: 最大重试=$MAX_RETRIES, 间隔=${RETRY_INTERVAL}s, 超时=${TIMEOUT}s"
attempt=0
while [[ $attempt -lt $MAX_RETRIES ]]; do
attempt=$((attempt + 1))
log_info "第 $attempt/$MAX_RETRIES 次尝试..."
# 执行检查
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
--connect-timeout "$TIMEOUT" \
--max-time "$TIMEOUT" \
"$URL" 2>/dev/null || echo "000")
log_info "HTTP 状态码: $HTTP_CODE"
# 判断结果
if [[ "$HTTP_CODE" == "200" ]] || [[ "$HTTP_CODE" == "302" ]]; then
log_info "✅ 健康检查通过!($URL → $HTTP_CODE)"
exit 0
fi
if [[ $attempt -lt $MAX_RETRIES ]]; then
log_warn "⏳ $RETRY_INTERVAL 秒后重试..."
sleep "$RETRY_INTERVAL"
fi
done
log_error "❌ 健康检查失败!($URL 经过 $MAX_RETRIES 次尝试后仍不可达)"
exit 1
使用示例:
# 检查本地服务(如果你有的话)
$ ./health_check.sh http://localhost:8080/health 3 2 5
# 检查外部网站
$ ./health_check.sh https://www.google.com
[2026-05-21 10:35:01] [INFO] 开始健康检查: https://www.google.com
[2026-05-21 10:35:01] [INFO] 参数: 最大重试=3, 间隔=2s, 超时=5s
[2026-05-21 10:35:01] [INFO] 第 1/3 次尝试...
[2026-05-21 10:35:02] [INFO] HTTP 状态码: 200
[2026-05-21 10:35:02] [INFO] ✅ 健康检查通过!(https://www.google.com → 200)
# 检查不存在的服务
$ ./health_check.sh http://localhost:9999/health 2 1 2
[2026-05-21 10:35:10] [INFO] 开始健康检查: http://localhost:9999/health
[2026-05-21 10:35:10] [INFO] 参数: 最大重试=2, 间隔=1s, 超时=2s
[2026-05-21 10:35:10] [INFO] 第 1/2 次尝试...
[2026-05-21 10:35:12] [INFO] HTTP 状态码: 000
[2026-05-21 10:35:12] [WARN] ⏳ 1 秒后重试...
[2026-05-21 10:35:13] [INFO] 第 2/2 次尝试...
[2026-05-21 10:35:15] [INFO] HTTP 状态码: 000
[2026-05-21 10:35:15] [ERROR] ❌ 健康检查失败!(http://localhost:9999/health 经过 2 次尝试后仍不可达)
考查要点:错误处理、curl 参数使用、带时间戳的日志、正确的退出码约定。
第 19 题:查找并清理 N 天前的日志文件
真题来源:美团运维面试题
题目:编写脚本查找 /var/log/myapp/ 下超过 30 天的 .log 文件,先压缩归档,再删除原文件。要求安全可靠。
点击查看解析
#!/bin/bash
# log_cleanup.sh — 日志归档清理
# 建议配置为 crontab 定期执行
set -euo pipefail
# ==================== 配置 ====================
LOG_DIR="${1:-/var/log/myapp}"
RETENTION_DAYS="${2:-30}"
ARCHIVE_DIR="${LOG_DIR}/archive"
# ==================== 前置检查 ====================
if [[ ! -d "$LOG_DIR" ]]; then
echo "错误: 日志目录 $LOG_DIR 不存在!" >&2
exit 1
fi
# 安全保护:禁止操作系统关键目录
case "$(realpath "$LOG_DIR")" in
/|/var|/var/log|/etc|/bin|/sbin|/usr|/root|/home)
echo "严重错误: 拒绝操作系统关键目录!" >&2
exit 1
;;
esac
# ==================== 主逻辑 ====================
echo "========================================="
echo "日志清理报告 — $(date '+%Y-%m-%d %H:%M:%S')"
echo "========================================="
echo "目标目录: $LOG_DIR"
echo "保留天数: $RETENTION_DAYS 天"
echo "归档目录: $ARCHIVE_DIR"
echo ""
# 创建归档目录
mkdir -p "$ARCHIVE_DIR"
# 查找旧日志
echo "--- 查找 ${RETENTION_DAYS} 天前的 .log 文件 ---"
OLD_FILES=$(find "$LOG_DIR" -maxdepth 1 -name "*.log" -mtime "+${RETENTION_DAYS}" -type f)
if [[ -z "$OLD_FILES" ]]; then
echo "没有找到过期日志文件。"
exit 0
fi
echo "$OLD_FILES" | while read -r file; do
echo " 找到: $file"
done
# 压缩归档
echo ""
echo "--- 压缩归档 ---"
echo "$OLD_FILES" | while IFS= read -r file; do
[[ -z "$file" ]] && continue
BASENAME=$(basename "$file")
ARCHIVE_NAME="${BASENAME}.$(date '+%Y%m%d').gz"
echo " 压缩: $BASENAME → $ARCHIVE_NAME"
gzip -c "$file" > "$ARCHIVE_DIR/$ARCHIVE_NAME"
echo " 删除: $BASENAME"
rm -f "$file"
done
# 统计报告
echo ""
echo "--- 清理完成 ---"
echo "归档文件数: $(find "$ARCHIVE_DIR" -name "*.gz" | wc -l | tr -d ' ')"
echo "归档目录大小: $(du -sh "$ARCHIVE_DIR" | cut -f1)"
exit 0
模拟测试:
# 创建测试环境
mkdir -p /tmp/test_logs
cd /tmp/test_logs
# 创建不同日期的模拟日志
touch -t 202601010000 old_log1.log # 模拟旧文件
touch -t 202601020000 old_log2.log
touch new_log.log # 今天的文件
# 运行清理脚本
$ ./log_cleanup.sh /tmp/test_logs 30
=========================================
日志清理报告 — 2026-05-21 10:40:00
=========================================
目标目录: /tmp/test_logs
保留天数: 30 天
归档目录: /tmp/test_logs/archive
--- 查找 30 天前的 .log 文件 ---
找到: /tmp/test_logs/old_log1.log
找到: /tmp/test_logs/old_log2.log
--- 压缩归档 ---
压缩: old_log1.log → old_log1.log.20260521.gz
删除: old_log1.log
压缩: old_log2.log → old_log2.log.20260521.gz
删除: old_log2.log
--- 清理完成 ---
归档文件数: 2
归档目录大小: 4.0K
考查要点:find 的 -mtime 参数、安全防护(禁止操作关键目录)、压缩归档流程、统计报告。
第 20 题:监控进程并自动重启
真题来源:字节跳动 SRE 面试题
题目:编写一个脚本,监控指定的进程是否存在,如果进程挂掉则自动重启,并记录每次重启事件。
点击查看解析
#!/bin/bash
# process_monitor.sh — 进程监控与自动重启
# 使用方式: ./process_monitor.sh <进程名> <启动命令> [检查间隔秒数]
set -euo pipefail
# ==================== 配置 ====================
PROCESS_NAME="${1:-}"
START_CMD="${2:-}"
CHECK_INTERVAL="${3:-5}"
LOG_FILE="/tmp/monitor_${PROCESS_NAME}.log"
MAX_RESTART_COUNT=5
RESTART_WINDOW=60 # 60 秒内最多重启 5 次,防止频繁重启
# ==================== 参数检查 ====================
if [[ -z "$PROCESS_NAME" ]] || [[ -z "$START_CMD" ]]; then
echo "用法: $0 <进程名> <启动命令> [检查间隔秒数]" >&2
echo "示例: $0 nginx 'systemctl start nginx' 5" >&2
exit 1
fi
# ==================== 日志函数 ====================
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
# ==================== 检查进程是否存在 ====================
is_running() {
pgrep -x "$PROCESS_NAME" > /dev/null 2>&1
}
# ==================== 主循环 ====================
log "=========================================="
log "进程监控启动"
log "进程名: $PROCESS_NAME"
log "启动命令: $START_CMD"
log "检查间隔: ${CHECK_INTERVAL}s"
log "=========================================="
restart_times=() # 记录重启时间戳
while true; do
if is_running; then
PID=$(pgrep -x "$PROCESS_NAME" | head -1)
log "[OK] $PROCESS_NAME 运行中 (PID: $PID)"
else
log "[FAIL] $PROCESS_NAME 未运行!"
# 检查重启频率
NOW=$(date +%s)
# 清理 60 秒前的记录
restart_times=($(for t in "${restart_times[@]}"; do
[[ $t -gt $((NOW - RESTART_WINDOW)) ]] && echo "$t"
done))
if [[ ${#restart_times[@]} -ge $MAX_RESTART_COUNT ]]; then
log "[FATAL] ${RESTART_WINDOW}秒内已重启${#restart_times[@]}次,超过上限!停止监控。"
exit 1
fi
# 尝试重启
log "[ACTION] 正在重启: $START_CMD"
if eval "$START_CMD"; then
restart_times+=("$NOW")
log "[OK] 重启成功"
else
log "[ERROR] 重启失败!"
fi
fi
sleep "$CHECK_INTERVAL"
done
使用示例:
# 模拟监控一个脚本进程
$ cat > test_daemon.sh << 'EOF'
#!/bin/bash
# 一个简单的守护进程模拟
echo "Daemon started, PID: $$"
trap 'echo "Daemon stopped"; exit' SIGTERM SIGINT
while true; do sleep 5; done
EOF
$ chmod +x test_daemon.sh
# 启动守护进程
$ ./test_daemon.sh &
[1] 45678
Daemon started, PID: 45678
# 在另一个终端启动监控
$ ./process_monitor.sh test_daemon.sh "./test_daemon.sh &" 3
[2026-05-21 10:45:00] ==========================================
[2026-05-21 10:45:00] 进程监控启动
[2026-05-21 10:45:00] 进程名: test_daemon.sh
[2026-05-21 10:45:00] 启动命令: ./test_daemon.sh &
[2026-05-21 10:45:00] 检查间隔: 3s
[2026-05-21 10:45:00] ==========================================
[2026-05-21 10:45:00] [OK] test_daemon.sh 运行中 (PID: 45678)
[2026-05-21 10:45:03] [OK] test_daemon.sh 运行中 (PID: 45678)
# 如果我们 kill 掉守护进程...
$ kill 45678
# 监控脚本输出:
[2026-05-21 10:45:10] [FAIL] test_daemon.sh 未运行!
[2026-05-21 10:45:10] [ACTION] 正在重启: ./test_daemon.sh &
[2026-05-21 10:45:10] [OK] 重启成功
[2026-05-21 10:45:13] [OK] test_daemon.sh 运行中 (PID: 46789)
考查要点:pgrep 进程检测、重启频率限制(防抖动)、日志记录、信号处理。
🔧 Shell 调试方法论速查
面试中面试官经常会问“你是怎么调试 Shell 脚本的”,完整的回答应该包含以下方法:
推荐脚本头部模板:
#!/bin/bash
set -euo pipefail
IFS=$'\n\t' # 只按换行和制表符分割,避免空格导致意外
trap 'echo "[ERROR] 行号 $LINENO 出错,退出码 $?" >&2' ERR
📋 面试速查清单
把下面这 20 个要点过一遍,看看自己是否都能说清楚:
💬 写在最后
Shell 脚本面试考的不是记忆力,而是你踩过多少坑、踩完之后学了多少。很多题目看似简单,但面试官只要稍稍追问——加一个带空格的文件名、换一个边界条件、丢一个 2GB 的文件过来——就能轻松区分“用过 Shell”和“理解 Shell”的候选人。
建议把本文中的每个脚本都在自己的终端中跑一遍,尤其是那些带 🚨 标记的错误示例——亲手踩一遍坑,比看十遍答案都管用。
另外,日常写脚本时建议养成用 shellcheck 做静态检查的习惯:
# 安装
# macOS: brew install shellcheck
# Ubuntu: apt install shellcheck
# 使用
shellcheck your_script.sh
很多面试官会问的问题,shellcheck 都能帮你提前发现。
最后留一个思考题:如果你发现线上一个 Shell 脚本正在死循环、CPU 被打满,你会怎么排查和处理? 欢迎在评论区留下你的思路~

浙公网安备 33010602011771号