大厂面试官最爱问的 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" 被拆分成了 helloworld 两个参数,导致语法错误。

# ❌ 错误
$ 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 题:exitreturn 用错会有什么后果?

面试官:“在函数中用 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.txtfile2.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 题:sedawk 的核心区别?什么时候用哪个?

面试官:“请说说 sedawk 的本质区别。举个例子。”

点击查看陷阱解析

用一句话总结:

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 被打满,你会怎么排查和处理? 欢迎在评论区留下你的思路~

posted @ 2026-05-21 22:47  IT策士  阅读(4)  评论(0)    收藏  举报