引子

最近帮团队面试了几位候选人,发现很多人在Shell脚本基础问题上栽了跟头。这些题目看似简单,却在日常开发中频繁使用,一旦理解不透彻就容易埋下隐患。今天就来聊聊这些面试中常见但容易被忽略的Shell脚本问题。

变量作用域的那些坑

子Shell中的变量传递

#!/bin/bash

count=0

echo "初始值: $count"

# 错误示范
count=10 | echo "管道中的值: $count"
echo "管道后的值: $count"

# 正确做法
count=20
echo "直接赋值后的值: $count"

# 使用命令替换
count=$(echo 30)
echo "命令替换后的值: $count"

关键点: 管道会创建子Shell,子Shell中的变量修改不会影响父Shell。这是很多脚本Bug的根源。

环境变量的正确使用

#!/bin/bash

# 错误:只在当前进程生效
MY_VAR="hello"
./another_script.sh

# 正确:传递给子进程
export MY_VAR="hello"
./another_script.sh

# 或者临时设置环境变量
MY_VAR="hello" ./another_script.sh

字符串处理的进阶技巧

参数扩展的高级用法

#!/bin/bash

filename="/path/to/somefile.txt"

# 获取文件名(不带路径)
echo "${filename##*/}"

# 获取目录路径
echo "${filename%/*}"

# 获取文件扩展名
echo "${filename##*.}"

# 变量默认值设置
unset maybe_empty
echo "${maybe_empty:-默认值}"

实战场景: 在处理文件路径、配置项时,这些参数扩展技巧能让代码更简洁健壮。

条件判断的细节差异

单中括号 vs 双中括号

#!/bin/bash

str="hello world"

# 单中括号 - 更传统,需要转义
if [ "$str" = "hello world" ]; then
    echo "匹配成功"
fi

# 双中括号 - 更现代,支持更多特性
if [[ $str == "hello world" ]]; then
    echo "匹配成功"
fi

# 模式匹配(只在双中括号中支持)
if [[ $str == h* ]]; then
    echo "以h开头"
fi

数值比较的特殊性

#!/bin/bash

num1=10
num2=20

# 错误:使用字符串比较符号
if [ "$num1" > "$num2" ]; then
    echo "错误比较"
fi

# 正确:使用数值比较符号
if [ "$num1" -gt "$num2" ]; then
    echo "num1 大于 num2"
else
    echo "num1 不大于 num2"
fi

错误处理的实战经验

立即退出与错误捕获

#!/bin/bash

# 任何命令失败立即退出
set -e

# 显示执行的命令(调试时很有用)
set -x

# 捕获未定义变量
set -u

# 组合使用
set -euxo pipefail

# 临时禁用错误退出
set +e
some_might_fail_command
set -e

信号处理与资源清理

#!/bin/bash

cleanup() {
    echo "正在清理临时文件..."
    rm -f /tmp/temp_file.*
    echo "清理完成"
}

# 注册信号处理函数
trap cleanup EXIT INT TERM

# 业务逻辑
echo "创建临时文件..."
touch /tmp/temp_file.$$

# 模拟长时间运行
sleep 10

函数返回值的隐藏规则

返回值与退出状态码

#!/bin/bash

# 函数返回字符串(错误做法)
get_message() {
    echo "Hello World"
}

result=$(get_message)
echo "结果: $result"

# 检查函数执行状态
if get_message > /dev/null; then
    echo "函数执行成功"
else
    echo "函数执行失败"
fi

# 正确设置退出状态码
check_file() {
    local file="$1"
    if [[ ! -f "$file" ]]; then
        echo "文件不存在: $file"
        return 1
    fi
    return 0
}

数组操作的实用技巧

数组遍历与操作

#!/bin/bash

# 数组定义
fruits=("apple" "banana" "orange")

# 遍历数组(索引方式)
for ((i=0; i<${#fruits[@]}; i++)); do
    echo "水果 $i: ${fruits[i]}"
done

# 遍历数组(值方式)
for fruit in "${fruits[@]}"; do
    echo "水果: $fruit"
done

# 数组切片
echo "前两个水果: ${fruits[@]:0:2}"

# 向数组添加元素
fruits+=("grape")

实战案例:安全的文件处理脚本

#!/bin/bash

set -euo pipefail

process_files() {
    local directory="${1:-.}"
    
    # 检查目录是否存在
    if [[ ! -d "$directory" ]]; then
        echo "错误: 目录不存在: $directory" >&2
        return 1
    fi
    
    # 安全地处理文件
    while IFS= read -r -d '' file; do
        if [[ -f "$file" && -r "$file" ]]; then
            echo "处理文件: $file"
            # 实际处理逻辑
            process_single_file "$file"
        fi
    done < <(find "$directory" -type f -print0)
}

process_single_file() {
    local file="$1"
    # 在这里实现具体的文件处理逻辑
    echo "正在处理: $file"
}

# 主程序
main() {
    local target_dir="${1:-}"
    
    if [[ -z "$target_dir" ]]; then
        echo "用法: $0 <目录>" >&2
        return 1
    fi
    
    process_files "$target_dir"
}

# 脚本入口
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi

总结思考

通过这些面试问题,我深刻体会到:Shell脚本的功力不在于会写多复杂的逻辑,而在于对基础概念的深刻理解和细节的精准把握。在实际工作中,那些看似简单的语法细节,往往决定了脚本的稳定性和可维护性。

希望这些经验对你有帮助,下次面试或者写脚本时,能够避开这些常见的陷阱。