1. 引言

Bash(Bourne Again SHell)是 Linux 和 macOS 系统中最常用的命令行解释器,也是编写自动化脚本的强大工具。掌握 Bash 脚本的基础语法是进行系统管理、任务自动化和 DevOps 工作的必备技能。

本文将系统性地介绍 Bash 脚本的基础语法,从最简单的脚本结构到变量、运算符、流程控制等核心概念,帮助读者快速上手 Bash 脚本编程。

2. Bash 脚本基础结构

2.1 脚本文件与执行权限

一个基本的 Bash 脚本通常以 .sh 为扩展名,文件开头需要指定解释器:

#!/bin/bash
# 这是一个 Bash 脚本示例
echo "Hello, World!"

执行权限设置:

# 给脚本添加执行权限
chmod +x script.sh

# 执行脚本
./script.sh
# 或者使用 bash 命令直接执行
bash script.sh

2.2 注释

Bash 使用 # 符号表示注释:

# 这是单行注释

: '
这是多行注释
可以跨越多行
'

# 行内注释
echo "Hello"  # 这是行内注释

3. 变量与数据类型

3.1 变量定义与使用

#!/bin/bash
# 定义变量(等号两边不能有空格)
name="John"
age=25
PI=3.14159

# 使用变量
echo "Name: $name"
echo "Age: $age"
echo "PI: $PI"

# 使用花括号避免歧义
echo "My name is ${name}Doe"

3.2 变量类型

Bash 主要支持以下变量类型:

字符串变量:

str1="Hello"
str2='World'
str3=$str1$str2  # 字符串拼接
echo "$str3"      # 输出: HelloWorld

整数变量:

num1=10
num2=20
sum=$((num1 + num2))  # 算术运算
echo "Sum: $sum"      # 输出: Sum: 30

数组变量:

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

# 访问数组元素
echo "First fruit: ${fruits[0]}"  # 输出: apple
echo "All fruits: ${fruits[@]}"   # 输出所有元素

# 数组长度
echo "Number of fruits: ${#fruits[@]}"  # 输出: 3

# 关联数组(Bash 4.0+)
declare -A person
person["name"]="Alice"
person["age"]=30
echo "Name: ${person["name"]}"  # 输出: Alice

3.3 特殊变量

#!/bin/bash
echo "脚本名称: $0"           # 脚本名称
echo "第一个参数: $1"         # 第一个参数
echo "第二个参数: $2"         # 第二个参数
echo "所有参数: $@"           # 所有参数列表
echo "参数个数: $#"           # 参数个数
echo "上一个命令退出状态: $?"  # 退出状态码(0表示成功)
echo "当前进程ID: $$"         # 当前进程ID
echo "后台进程ID: $!"         # 最后一个后台进程ID

3.4 环境变量与局部变量

#!/bin/bash
# 环境变量(全局)
export GLOBAL_VAR="I'm global"

# 局部变量(默认)
local_var="I'm local"

# 在函数中使用 local 声明局部变量
my_function() {
    local func_var="I'm local to function"
    echo "$func_var"
}

my_function
echo "$func_var"  # 这里会输出空,因为 func_var 是函数局部变量

4. 输入与输出

4.1 基本输出

#!/bin/bash
# echo 命令
echo "Hello, World!"
echo -e "Line 1\nLine 2"  # -e 启用转义字符
echo -n "No newline"       # -n 不换行

# printf 命令(格式化输出)
printf "Name: %s\n" "Alice"
printf "Age: %d, Height: %.2f\n" 25 175.5

4.2 用户输入

#!/bin/bash
# read 命令读取用户输入
echo "请输入您的名字:"
read name
echo "Hello, $name!"

# 单行读取
read -p "请输入年龄: " age
echo "年龄: $age"

# 读取多个值
read -p "请输入姓名和年龄: " name age
echo "姓名: $name, 年龄: $age"

# 静默读取(用于密码)
read -s -p "请输入密码: " password
echo -e "\n密码已输入"

4.3 文件重定向

#!/bin/bash
# 输出重定向
echo "Hello" > output.txt      # 覆盖写入
echo "World" >> output.txt     # 追加写入

# 输入重定向
wc -l < output.txt             # 统计行数

# 错误重定向
command_not_exist 2> error.log  # 将错误信息重定向到文件
command 2>&1                    # 将错误重定向到标准输出
command &> all_output.log       # 将标准和错误输出都重定向

# Here Document
cat << EOF
这是多行文本
第二行
第三行
EOF

5. 运算符

5.1 算术运算符

#!/bin/bash
a=10
b=3

# 使用 $(( )) 进行算术运算
echo "加法: $((a + b))"      # 13
echo "减法: $((a - b))"      # 7
echo "乘法: $((a * b))"      # 30
echo "除法: $((a / b))"      # 3(整数除法)
echo "取余: $((a % b))"      # 1
echo "指数: $((a ** b))"     # 1000

# 使用 let 命令
let "c = a + b"
echo "c = $c"                # 13

# 使用 expr 命令(较旧的方式)
result=$(expr $a + $b)
echo "expr 结果: $result"    # 13

5.2 关系运算符

#!/bin/bash
x=10
y=20

# 数值比较
if [ $x -eq $y ]; then echo "x 等于 y"; fi
if [ $x -ne $y ]; then echo "x 不等于 y"; fi
if [ $x -lt $y ]; then echo "x 小于 y"; fi
if [ $x -le $y ]; then echo "x 小于等于 y"; fi
if [ $x -gt $y ]; then echo "x 大于 y"; fi
if [ $x -ge $y ]; then echo "x 大于等于 y"; fi

# 字符串比较
str1="hello"
str2="world"

if [ "$str1" = "$str2" ]; then echo "字符串相等"; fi
if [ "$str1" != "$str2" ]; then echo "字符串不相等"; fi
if [ -z "$str1" ]; then echo "字符串为空"; fi
if [ -n "$str1" ]; then echo "字符串非空"; fi
if [ "$str1" \< "$str2" ]; then echo "str1 在字典序中小于 str2"; fi

5.3 逻辑运算符

#!/bin/bash
a=10
b=20
c=30

# 逻辑与
if [ $a -lt $b ] && [ $b -lt $c ]; then
    echo "a < b < c"
fi

# 逻辑或
if [ $a -gt $b ] || [ $a -lt $c ]; then
    echo "a > b 或 a < c"
fi

# 逻辑非
if ! [ $a -eq $b ]; then
    echo "a 不等于 b"
fi

# 使用 [[ ]] 的扩展语法
if [[ $a -lt $b && $b -lt $c ]]; then
    echo "使用 [[ ]] 语法: a < b < c"
fi

5.4 文件测试运算符

#!/bin/bash
file="/etc/passwd"
dir="/tmp"

# 文件测试
if [ -e "$file" ]; then echo "文件存在"; fi
if [ -f "$file" ]; then echo "是普通文件"; fi
if [ -d "$dir" ]; then echo "是目录"; fi
if [ -s "$file" ]; then echo "文件大小大于0"; fi
if [ -r "$file" ]; then echo "文件可读"; fi
if [ -w "$file" ]; then echo "文件可写"; fi
if [ -x "$file" ]; then echo "文件可执行"; fi

# 特殊文件测试
if [ -L "$file" ]; then echo "是符号链接"; fi
if [ -b "$file" ]; then echo "是块设备文件"; fi
if [ -c "$file" ]; then echo "是字符设备文件"; fi
if [ -p "$file" ]; then echo "是命名管道"; fi

6. 字符串操作

6.1 字符串长度与子串

#!/bin/bash
str="Hello World"

# 字符串长度
echo "长度: ${#str}"                    # 11

# 提取子串
echo "从索引2开始: ${str:2}"            # llo World
echo "从索引2开始取5个字符: ${str:2:5}"  # llo W

# 从右边开始提取
echo "最后5个字符: ${str: -5}"          # World

6.2 字符串替换

#!/bin/bash
str="apple orange apple banana"

# 替换第一个匹配
echo "${str/apple/pear}"      # pear orange apple banana

# 替换所有匹配
echo "${str//apple/pear}"     # pear orange pear banana

# 前缀匹配替换
echo "${str/#apple/pear}"     # pear orange apple banana

# 后缀匹配替换
echo "${str/%banana/grape}"   # apple orange apple grape

6.3 字符串删除

#!/bin/bash
str="Hello World Hello Bash"

# 删除最短匹配的前缀
echo "${str#Hello}"           # " World Hello Bash"

# 删除最长匹配的前缀
echo "${str##*Hello}"         # " Bash"

# 删除最短匹配的后缀
echo "${str% Bash}"           # "Hello World Hello"

# 删除最长匹配的后缀
echo "${str%% World*}"        # "Hello"

6.4 大小写转换

#!/bin/bash
str="Hello World"

# 转换为大写
echo "${str^^}"               # HELLO WORLD
echo "${str^^[hw]}"           # HEllo World (仅h,w转大写)

# 转换为小写
echo "${str,,}"               # hello world
echo "${str,,[HW]}"           # hello world (仅H,W转小写)

# 首字母大写
echo "${str^}"                # Hello World

7. 数组操作

7.1 数组基本操作

#!/bin/bash
# 定义数组
colors=("red" "green" "blue" "yellow")

# 访问元素
echo "第一个颜色: ${colors[0]}"    # red
echo "所有颜色: ${colors[@]}"      # red green blue yellow

# 修改元素
colors[1]="purple"
echo "修改后: ${colors[@]}"        # red purple blue yellow

# 添加元素
colors+=("orange")
echo "添加后: ${colors[@]}"        # red purple blue yellow orange

# 删除元素
unset colors[2]
echo "删除后: ${colors[@]}"        # red purple yellow orange

7.2 数组遍历

#!/bin/bash
fruits=("apple" "banana" "orange" "grape")

# 使用 for 循环遍历
echo "方法1: 直接遍历"
for fruit in "${fruits[@]}"; do
    echo "水果: $fruit"
done

echo -e "\n方法2: 使用索引"
for i in "${!fruits[@]}"; do
    echo "索引 $i: ${fruits[i]}"
done

echo -e "\n方法3: C风格for循环"
for ((i=0; i<${#fruits[@]}; i++)); do
    echo "水果 $((i+1)): ${fruits[i]}"
done

7.3 关联数组

#!/bin/bash
# 声明关联数组(Bash 4.0+)
declare -A student

# 添加键值对
student["name"]="Alice"
student["age"]=20
student["grade"]="A"

# 访问值
echo "学生姓名: ${student["name"]}"
echo "学生年龄: ${student["age"]}"

# 遍历关联数组
echo -e "\n所有信息:"
for key in "${!student[@]}"; do
    echo "$key: ${student[$key]}"
done

8. 函数

8.1 函数定义与调用

#!/bin/bash
# 函数定义
greet() {
    echo "Hello, $1!"
}

# 函数调用
greet "Alice"      # 输出: Hello, Alice!
greet "Bob"        # 输出: Hello, Bob!

# 带返回值的函数
add() {
    local sum=$(( $1 + $2 ))
    echo $sum
}

# 调用并获取返回值
result=$(add 10 20)
echo "10 + 20 = $result"  # 输出: 10 + 20 = 30

8.2 函数参数

#!/bin/bash
# 函数参数
show_info() {
    echo "函数名称: $0"      # 脚本名称,不是函数名称
    echo "第一个参数: $1"
    echo "第二个参数: $2"
    echo "所有参数: $@"
    echo "参数个数: $#"
}

show_info "Alice" 25 "Engineer"

# 使用 shift 处理参数
process_args() {
    while [ $# -gt 0 ]; do
        echo "处理参数: $1"
        shift
    done
}

process_args arg1 arg2 arg3

8.3 函数返回值

#!/bin/bash
# 使用 return 返回状态码
is_even() {
    local num=$1
    if [ $((num % 2)) -eq 0 ]; then
        return 0  # 成功/真
    else
        return 1  # 失败/假
    fi
}

# 检查返回值
if is_even 10; then
    echo "10 是偶数"
else
    echo "10 是奇数"
fi

# 使用 echo 返回数据
get_user_info() {
    local username=$1
    echo "姓名: $username"
    echo "年龄: 30"
    echo "职业: 工程师"
}

# 捕获多行输出
info=$(get_user_info "Alice")
echo "用户信息:"
echo "$info"

9. 流程控制基础

9.1 条件判断

#!/bin/bash
# if 语句
age=18

if [ $age -ge 18 ]; then
    echo "成年人"
fi

# if-else 语句
if [ $age -ge 18 ]; then
    echo "成年人"
else
    echo "未成年人"
fi

# if-elif-else 语句
score=85

if [ $score -ge 90 ]; then
    echo "优秀"
elif [ $score -ge 80 ]; then
    echo "良好"
elif [ $score -ge 60 ]; then
    echo "及格"
else
    echo "不及格"
fi

9.2 循环结构

#!/bin/bash
# for 循环
echo "for 循环示例:"
for i in 1 2 3 4 5; do
    echo "数字: $i"
done

# while 循环
echo -e "\nwhile 循环示例:"
count=1
while [ $count -le 5 ]; do
    echo "计数: $count"
    ((count++))
done

# until 循环
echo -e "\nuntil 循环示例:"
count=1
until [ $count -gt 5 ]; do
    echo "计数: $count"
    ((count++))
done

9.3 case 语句

#!/bin/bash
# case 语句
fruit="apple"

case $fruit in
    "apple")
        echo "这是苹果"
        ;;
    "banana")
        echo "这是香蕉"
        ;;
    "orange"|"grape")
        echo "这是橘子或葡萄"
        ;;
    *)
        echo "未知水果"
        ;;
esac

10. 实用示例

10.1 简单的文件备份脚本

#!/bin/bash
# 文件备份脚本
BACKUP_DIR="/backup"
SOURCE_FILE="/etc/passwd"
BACKUP_FILE="$BACKUP_DIR/passwd_backup_$(date +%Y%m%d).bak"

# 检查备份目录
if [ ! -d "$BACKUP_DIR" ]; then
    echo "创建备份目录: $BACKUP_DIR"
    mkdir -p "$BACKUP_DIR"
fi

# 备份文件
if