跳转至

Shell 脚本参考资料

引言

本文是 Bash Shell 脚本的综合参考手册,涵盖变量与数组、条件判断、循环、函数、字符串操作、文件测试、正则表达式等核心语法,并收录常用单行命令、推荐工具和编码风格指南。无论是编写简单的自动化脚本还是复杂的机器人部署工具,都可以在此快速查阅所需语法。

变量与数据类型

变量基础

# 变量赋值(等号两侧不能有空格)
NAME="robot_01"
COUNT=42

# 只读变量
readonly PI=3.14159

# 引用变量
echo "机器人名称:$NAME"
echo "机器人名称:${NAME}"     # 花括号形式,避免歧义

# 删除变量
unset COUNT

# 环境变量(子进程可见)
export ROS_DOMAIN_ID=42

# 命令替换
CURRENT_DIR=$(pwd)
FILE_COUNT=$(ls -1 | wc -l)
TIMESTAMP=$(date '+%Y%m%d_%H%M%S')

# 默认值
ROBOT_NAME=${ROBOT_NAME:-"default_robot"}   # 未设置时使用默认值
ROBOT_IP=${ROBOT_IP:="192.168.1.100"}       # 未设置时赋值并使用

特殊变量

变量 含义
$0 脚本文件名
$1 ~ $9 位置参数(第 1 至第 9 个参数)
${10} 第 10 个及以后的参数需花括号
$# 参数个数
$@ 所有参数(各自独立引用)
$* 所有参数(作为一个整体)
$? 上一个命令的退出状态码
$$ 当前脚本的 PID(进程标识符)
$! 最近一个后台进程的 PID
$_ 上一个命令的最后一个参数

数组

# 索引数组
SENSORS=("lidar" "imu" "camera" "gps")

# 访问元素
echo "${SENSORS[0]}"        # lidar
echo "${SENSORS[-1]}"       # gps(最后一个元素)

# 所有元素
echo "${SENSORS[@]}"

# 数组长度
echo "${#SENSORS[@]}"       # 4

# 添加元素
SENSORS+=("ultrasonic")

# 遍历数组
for sensor in "${SENSORS[@]}"; do
    echo "传感器:$sensor"
done

# 数组切片
echo "${SENSORS[@]:1:2}"    # imu camera

# 关联数组(Bash 4.0 及以上)
declare -A SENSOR_PORTS
SENSOR_PORTS[lidar]="/dev/rplidar"
SENSOR_PORTS[imu]="/dev/imu"
SENSOR_PORTS[motor]="/dev/motor_controller"

# 遍历关联数组
for key in "${!SENSOR_PORTS[@]}"; do
    echo "$key -> ${SENSOR_PORTS[$key]}"
done

条件判断

if 语句

# 基本 if-elif-else 结构
if [ "$ROBOT_MODE" = "auto" ]; then
    echo "自动驾驶模式"
elif [ "$ROBOT_MODE" = "manual" ]; then
    echo "手动控制模式"
else
    echo "未知模式:$ROBOT_MODE"
fi

# 使用 [[ ]] 进行更安全的判断(支持模式匹配)
if [[ "$HOSTNAME" == robot-* ]]; then
    echo "这是一台机器人主机"
fi

# 逻辑组合
if [[ -f "/dev/rplidar" && -f "/dev/imu" ]]; then
    echo "所有传感器设备就绪"
fi

数值比较运算符

运算符 含义 示例
-eq 等于 [ "$a" -eq "$b" ]
-ne 不等于 [ "$a" -ne "$b" ]
-gt 大于 [ "$a" -gt "$b" ]
-ge 大于等于 [ "$a" -ge "$b" ]
-lt 小于 [ "$a" -lt "$b" ]
-le 小于等于 [ "$a" -le "$b" ]

字符串比较运算符

运算符 含义 示例
= / == 相等 [ "$a" = "$b" ]
!= 不相等 [ "$a" != "$b" ]
-z 长度为零 [ -z "$a" ]
-n 长度非零 [ -n "$a" ]
< 字典序小于(需 [[ ]] [[ "$a" < "$b" ]]
> 字典序大于(需 [[ ]] [[ "$a" > "$b" ]]

case 语句

case "$1" in
    start)
        echo "启动机器人..."
        ;;
    stop)
        echo "停止机器人..."
        ;;
    restart)
        echo "重启机器人..."
        ;;
    status)
        echo "查询状态..."
        ;;
    *)
        echo "用法:$0 {start|stop|restart|status}"
        exit 1
        ;;
esac

循环

for 循环

# 遍历列表
for node in navigation perception localization; do
    echo "启动节点:$node"
done

# C 风格 for 循环
for ((i=0; i<10; i++)); do
    echo "迭代 $i"
done

# 遍历文件
for bag_file in /data/bags/*.bag; do
    echo "处理:$bag_file"
    rosbag info "$bag_file"
done

# 遍历命令输出
for pid in $(pgrep -f "ros2"); do
    echo "ROS 2 进程:$pid"
done

# 使用 seq 生成数列
for i in $(seq 1 5); do
    echo "第 $i 次测试"
done

while 循环

# 基本 while 循环
counter=0
while [ "$counter" -lt 10 ]; do
    echo "计数:$counter"
    counter=$((counter + 1))
done

# 读取文件逐行处理
while IFS= read -r line; do
    echo "配置项:$line"
done < /home/robot/config/params.txt

# 无限循环
while true; do
    ros2 topic echo /odom --once
    sleep 1
done

# 等待条件满足
while ! ping -c 1 -W 1 192.168.1.100 > /dev/null 2>&1; do
    echo "等待网络连接..."
    sleep 2
done
echo "网络已连接"

until 循环

# until:条件为假时执行
until ros2 node list 2>/dev/null | grep -q "robot_state_publisher"; do
    echo "等待 robot_state_publisher 启动..."
    sleep 1
done
echo "节点已就绪"

函数

# 函数定义
check_sensor() {
    local device="$1"       # local 声明局部变量
    local name="$2"

    if [ -e "$device" ]; then
        echo "[OK] $name ($device) 已连接"
        return 0
    else
        echo "[FAIL] $name ($device) 未检测到"
        return 1
    fi
}

# 调用函数
check_sensor "/dev/rplidar" "激光雷达"
check_sensor "/dev/imu" "惯性测量单元"

# 获取函数返回值
if check_sensor "/dev/rplidar" "激光雷达"; then
    echo "可以启动导航"
fi

# 函数返回字符串(通过 echo 捕获)
get_robot_ip() {
    local interface="${1:-eth0}"
    ip addr show "$interface" | grep -oP 'inet \K[\d.]+'
}

ROBOT_IP=$(get_robot_ip "wlan0")
echo "机器人 IP:$ROBOT_IP"

# 带默认参数的函数
log_message() {
    local level="${1:-INFO}"
    local message="$2"
    echo "[$(date '+%H:%M:%S')] [$level] $message"
}

log_message "INFO" "系统启动"
log_message "ERROR" "传感器连接失败"

字符串操作

STR="Hello_Robot_World"

# 字符串长度
echo "${#STR}"                 # 17

# 子字符串截取
echo "${STR:6:5}"              # Robot

# 删除前缀(最短匹配)
FILE="/home/robot/data/map.yaml"
echo "${FILE#*/}"              # home/robot/data/map.yaml

# 删除前缀(最长匹配)
echo "${FILE##*/}"             # map.yaml(相当于 basename)

# 删除后缀(最短匹配)
echo "${FILE%/*}"              # /home/robot/data(相当于 dirname)

# 删除后缀(最长匹配)
echo "${FILE%%/*}"             # (空,因为以 / 开头)

# 替换(第一个匹配)
echo "${STR/Robot/Drone}"      # Hello_Drone_World

# 替换(所有匹配)
echo "${STR//_/-}"             # Hello-Robot-World

# 大小写转换(Bash 4.0 及以上)
NAME="robot"
echo "${NAME^^}"               # ROBOT(全部大写)
echo "${NAME^}"                # Robot(首字母大写)
UPPER="ROBOT"
echo "${UPPER,,}"              # robot(全部小写)

文件测试运算符

运算符 含义
-e file 文件存在
-f file 是普通文件
-d file 是目录
-L file 是符号链接
-r file 可读
-w file 可写
-x file 可执行
-s file 文件大小大于零
-b file 块设备
-c file 字符设备
-p file 命名管道
-S file 套接字文件
file1 -nt file2 file1 比 file2 新
file1 -ot file2 file1 比 file2 旧

使用示例:

CONFIG="/home/robot/config/nav_params.yaml"

if [ -f "$CONFIG" ] && [ -r "$CONFIG" ]; then
    echo "配置文件存在且可读"
elif [ -d "$CONFIG" ]; then
    echo "错误:这是一个目录而非文件"
else
    echo "错误:配置文件不存在"
fi

# 检查设备文件
if [ -c "/dev/ttyUSB0" ]; then
    echo "串口设备就绪"
fi

常用单行命令

# 查找并替换文件内容
grep -rl "old_param" /home/robot/config/ | xargs sed -i 's/old_param/new_param/g'

# 统计代码行数(排除空行和注释)
find src/ -name "*.py" | xargs grep -v -E '^\s*(#|$)' | wc -l

# 批量重命名文件
for f in *.JPG; do mv "$f" "${f%.JPG}.jpg"; done

# 监控文件变化
inotifywait -m /home/robot/config/ -e modify -e create | while read dir event file; do
    echo "[$(date)] $dir$file 发生 $event 事件"
done

# 并行执行命令(使用 xargs)
cat hosts.txt | xargs -P 4 -I {} ssh {} "uptime"

# 查看最近修改的文件
find /home/robot/logs -type f -mmin -30 -ls

# 比较两个目录的差异
diff <(ls dir1/) <(ls dir2/)

# 快速创建指定大小的测试文件
dd if=/dev/zero of=test_100mb.bin bs=1M count=100

# 按列排序 CSV 文件(按第 3 列数值降序)
sort -t',' -k3 -rn data.csv

# 提取 IP 地址
grep -oP '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' /var/log/syslog | sort -u

# 实时跟踪多个日志文件
tail -f /home/robot/logs/*.log

# 计算文件 MD5 校验和
find firmware/ -type f | xargs md5sum > checksums.md5

Bash 正则表达式

Bash 3.0 及以上版本支持 =~ 运算符进行正则匹配,需要在 [[ ]] 中使用。

# 基本正则匹配
IP="192.168.1.100"
if [[ "$IP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    echo "有效的 IP 地址格式"
fi

# 使用捕获组
LOG_LINE="[2026-03-21 14:30:00] ERROR: sensor timeout"
if [[ "$LOG_LINE" =~ \[([0-9-]+)\ ([0-9:]+)\]\ ([A-Z]+):\ (.*) ]]; then
    DATE="${BASH_REMATCH[1]}"      # 2026-03-21
    TIME="${BASH_REMATCH[2]}"      # 14:30:00
    LEVEL="${BASH_REMATCH[3]}"     # ERROR
    MESSAGE="${BASH_REMATCH[4]}"   # sensor timeout
    echo "日期=$DATE 时间=$TIME 级别=$LEVEL 消息=$MESSAGE"
fi

# 验证 ROS 话题名称格式
TOPIC="/robot/cmd_vel"
if [[ "$TOPIC" =~ ^/[a-zA-Z_][a-zA-Z0-9_/]*$ ]]; then
    echo "有效的 ROS 话题名称"
fi

# 提取版本号
VERSION_STR="ROS 2 Humble Hawksbill (22.04)"
if [[ "$VERSION_STR" =~ ([0-9]+)\.([0-9]+) ]]; then
    MAJOR="${BASH_REMATCH[1]}"
    MINOR="${BASH_REMATCH[2]}"
    echo "版本:${MAJOR}.${MINOR}"
fi

常用正则表达式元字符:

元字符 含义 示例
. 任意单个字符 a.c 匹配 abc、aXc
* 前一字符零次或多次 ab*c 匹配 ac、abc、abbc
+ 前一字符一次或多次 ab+c 匹配 abc、abbc
? 前一字符零次或一次 ab?c 匹配 ac、abc
^ 行首 ^Error
$ 行尾 done$
[abc] 字符集合 [aeiou] 匹配元音
[^abc] 排除字符集合 [^0-9] 匹配非数字
\d 数字(等价于 [0-9]
\w 单词字符
\s 空白字符
{n,m} 重复 n 到 m 次 a{2,4} 匹配 aa、aaa、aaaa
(...) 分组和捕获
\| cat\|dog

ShellCheck 静态分析

ShellCheck 是一款强大的 Shell 脚本静态分析工具,能自动检测常见错误和不良实践。

# 安装 ShellCheck
sudo apt install shellcheck

# 检查脚本
shellcheck my_script.sh

# 检查时排除特定规则
shellcheck -e SC2034,SC2086 my_script.sh

# 指定 Shell 类型
shellcheck --shell=bash my_script.sh

# 输出为 JSON 格式(用于 CI 集成)
shellcheck -f json my_script.sh

ShellCheck 常见警告及修复:

编号 问题 修复前 修复后
SC2086 变量未加引号 rm $file rm "$file"
SC2046 命令替换未加引号 rm $(find . -name "*.tmp") 改用 find ... -exec
SC2034 变量已赋值但未使用 unused_var=1 删除或使用该变量
SC2164 cd 可能失败 cd /some/dir cd /some/dir \|\| exit 1
SC2155 声明和赋值应分开 local x=$(cmd) local x; x=$(cmd)

编码风格指南

以下风格建议参考 Google Shell Style Guide,适用于机器人项目的脚本编写。

文件头

#!/bin/bash
# 文件名:deploy_robot.sh
# 描述:机器人软件部署脚本
# 作者:Team Name
# 日期:2026-03-21
set -euo pipefail    # 严格模式:出错即退出,未定义变量报错,管道错误传播

命名规范

# 变量:小写下划线分隔
robot_name="nav_bot"
sensor_count=5

# 常量和环境变量:大写下划线分隔
readonly MAX_RETRY=3
export ROS_DOMAIN_ID=42

# 函数:小写下划线分隔,使用动词开头
start_navigation() { ... }
check_sensor_status() { ... }
get_robot_ip() { ... }

最佳实践

# 1. 始终引用变量
echo "文件:$file"           # 不好(在某些情况下会出错)
echo "文件:${file}"         # 较好
echo "文件:\"${file}\""     # 路径含空格时最安全

# 2. 使用 $() 替代反引号
date=$(date +%Y%m%d)         # 推荐
date=`date +%Y%m%d`          # 不推荐(嵌套困难)

# 3. 使用 [[ ]] 替代 [ ]
if [[ -f "$file" ]]; then    # 推荐(更安全,支持更多特性)
if [ -f "$file" ]; then      # 不推荐

# 4. 算术运算使用 $(())
result=$((a + b * c))        # 推荐
result=$(expr $a + $b)       # 不推荐

# 5. 函数使用 local 声明局部变量
my_func() {
    local input="$1"         # 避免污染全局作用域
    local result
    result=$(process "$input")
    echo "$result"
}

# 6. 提供有意义的退出码
EXIT_SUCCESS=0
EXIT_CONFIG_ERROR=1
EXIT_DEVICE_ERROR=2
EXIT_NETWORK_ERROR=3

# 7. 使用 trap 确保清理
cleanup() {
    rm -f "$TEMP_FILE"
    echo "清理完成"
}
trap cleanup EXIT

推荐工具与资源

类别 工具/资源 说明
静态分析 ShellCheck Shell 脚本 lint 工具
格式化 shfmt Shell 脚本自动格式化
调试 bash -x script.sh 逐行打印执行过程
调试 set -x / set +x 在脚本内部开关调试输出
在线工具 ExplainShell 命令行语法在线解析
参考手册 Bash Reference Manual GNU 官方手册
教程 The Linux Command Line 入门教材
高级教程 Advanced Bash-Scripting Guide 进阶脚本编程
代码风格 Google Shell Style Guide Google 编码规范
速查表 devhints.io/bash Bash 语法速查

调试技巧:

# 方法 1:命令行开启调试
bash -x ./my_script.sh

# 方法 2:在脚本中开关调试
set -x          # 开启调试输出
# ... 需要调试的代码 ...
set +x          # 关闭调试输出

# 方法 3:使用 PS4 自定义调试前缀
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
bash -x ./my_script.sh

# 方法 4:使用 trap DEBUG 逐行追踪
trap 'echo "DEBUG: 行 $LINENO: $BASH_COMMAND"' DEBUG

参考资料