强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Bash 脚本编写教程 / 02 - 基础语法

02 - 基础语法

2.1 脚本的基本结构

一个规范的 Bash 脚本通常包含以下部分:

#!/bin/bash
# ============================================================
# 脚本名称: deploy.sh
# 描述: 自动化部署脚本
# 用法: ./deploy.sh [环境] [版本]
# 作者: 张三
# 日期: 2026-05-10
# ============================================================

set -euo pipefail  # 严格模式

# ---- 常量定义 ----
readonly VERSION="1.0.0"
readonly LOG_FILE="/var/log/deploy.log"

# ---- 函数定义 ----
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

# ---- 主逻辑 ----
main() {
    log "部署开始,目标环境: ${1:-production}"
    # ... 业务逻辑 ...
    log "部署完成"
}

main "$@"

2.2 Shebang(#!)

Shebang 是脚本第一行的 #! 序列,告诉操作系统用哪个解释器来执行脚本。

Shebang含义适用场景
#!/bin/bash使用绝对路径的 Bash最常见的写法,确保使用 Bash
#!/usr/bin/env bash通过 env 查找 Bash更具可移植性(推荐)
#!/bin/sh使用系统默认 ShellPOSIX 兼容脚本
#!/usr/bin/env python3Python 脚本混合项目中的 Python 部分
#!/usr/bin/env -S bash --norc --noprofile带选项的 Bash需要干净环境时

Shebang 的重要性

# ❌ 没有 Shebang,执行方式取决于调用者
echo 'echo "hello"' > test.sh
bash test.sh   # 用 bash 执行
sh test.sh     # 用 sh 执行(行为可能不同!)
./test.sh      # 用默认 shell 执行(可能是 dash)

# ✅ 有 Shebang,行为一致
echo '#!/bin/bash' > test.sh
echo 'echo "hello"' >> test.sh
chmod +x test.sh
./test.sh      # 始终用 bash 执行

⚠️ 注意:Shebang 行只在文件作为可执行程序运行时生效。通过 bash script.sh 方式调用时,Shebang 被忽略。

不同系统的 Bash 路径

系统Bash 路径建议
Linux/bin/bash#!/bin/bash#!/usr/bin/env bash
macOS/bin/bash(旧版 3.2)#!/usr/bin/env bash(配合 Homebrew)
FreeBSD/usr/local/bin/bash#!/usr/bin/env bash
WSL/bin/bash#!/bin/bash

💡 最佳实践:优先使用 #!/usr/bin/env bash 以获得最佳可移植性。

2.3 变量基础

变量赋值

# ✅ 正确:等号两边没有空格
name="张三"
age=25
path="/usr/local/bin"

# ❌ 错误:等号两边有空格会被解析为命令
# name = "张三"  # 报错:name: command not found

# 动态赋值
current_date=$(date +%Y-%m-%d)
file_count=$(ls -1 | wc -l)

变量使用

name="World"

# 方式一:直接引用
echo $name

# 方式二:花括号(推荐,明确边界)
echo "Hello, ${name}!"

# 何时必须用花括号
prefix="file"
echo "${prefix}name.txt"   # 输出: filename.txt
echo "$prefixname.txt"     # ❌ 错误:尝试引用变量 $prefixname

# 只读变量
readonly PI=3.14159
# PI=3.14  # 报错:PI: readonly variable

变量命名规范

规则示例说明
✅ 字母/下划线开头my_var, _count合法命名
✅ 包含数字var1, count2合法但不能以数字开头
❌ 以数字开头1var非法
❌ 包含连字符my-var被解析为减法
❌ 包含空格my var语法错误
⚠️ 全大写PATH, HOME保留给环境变量和常量
# 命名风格建议
readonly MAX_RETRY=3          # 常量:全大写+下划线
file_path="/tmp/test.txt"     # 变量:小写+下划线
userName="admin"              # 变量:驼峰(可选)
local attempt_count=0         # 局部变量:小写+下划线

2.4 引号规则

Bash 中有三种引号,行为截然不同:

双引号(")—— 弱引用

name="World"
echo "Hello, $name!"        # 输出: Hello, World!
echo "当前路径: $(pwd)"      # 输出: 当前路径: /home/user
echo "10 * 5 = $((10 * 5))" # 输出: 10 * 5 = 50

# 双引号保留空格和特殊字符
greeting="Hello   World"
echo "$greeting"            # 输出: Hello   World(保留空格)
echo $greeting              # 输出: Hello World(空格被压缩)

单引号(’)—— 强引用

name="World"
echo 'Hello, $name!'        # 输出: Hello, $name!(不展开)
echo '当前路径: $(pwd)'      # 输出: 当前路径: $(pwd)(不展开)
echo 'It'\''s a test'       # 输出: It's a test(单引号转义技巧)

# 在单引号中嵌入单引号的三种方法
echo 'It'"'"'s'             # 方法一:用双引号包裹单引号
echo 'It'\''s'             # 方法二:中断+转义+继续
echo $'It\'s'              # 方法三:$'' 语法

反引号(`)与 $() —— 命令替换

# 反引号(旧语法,不推荐)
today=`date +%Y-%m-%d`

# $()(推荐,可嵌套)
today=$(date +%Y-%m-%d)

# 嵌套示例
files_in_dir=$(ls $(dirname "/etc/hosts"))

引号速查表

场景推荐示例
变量赋值双引号file="$1"
命令替换$()date=$(date)
字面字符串单引号pattern='[0-9]+'
需要展开的字符串双引号msg="Hello, $name"
包含特殊字符单引号regex='^start.*end$'
命令参数双引号rm "$file"

⚠️ 黄金法则:变量引用永远加双引号 "$var",除非你有明确理由不这样做。

2.5 命令替换

命令替换允许将命令的输出赋值给变量或嵌入到字符串中。

# 基本用法
current_user=$(whoami)
kernel_version=$(uname -r)
ip_address=$(hostname -I | awk '{print $1}')

# 嵌套使用
log_file="/var/log/$(basename "$0").log"

# 在字符串中使用
echo "运行在 $(hostname) 上,当前时间 $(date '+%H:%M')"

# 多行命令
file_info=$(
    echo "=== 文件信息 ==="
    ls -lh /etc/hosts
    echo "=== 文件类型 ==="
    file /etc/hosts
)

# 赋值给数组
files=($(ls -1 *.txt))   # ⚠️ 文件名有空格时会出问题

命令替换的注意事项

# ❌ 空格问题
files=$(ls -1)     # 输出是一个字符串,不是数组

# ✅ 正确:使用 mapfile/readarray 填充数组
mapfile -t files < <(ls -1)

# ❌ 未加引号导致分词
path=$(get_path)
cd $path            # 如果路径有空格会出错

# ✅ 正确:加双引号
cd "$path"

# ❌ 反引号嵌套困难
# result=`echo \`date\``  # 难以阅读

# ✅ 使用 $() 可以自然嵌套
result=$(echo $(date))

2.6 分号、换行与命令分隔

# 分号分隔:同一行执行多条命令
echo "开始"; date; echo "结束"

# 换行分隔(推荐,更清晰)
echo "开始"
date
echo "结束"

# 逻辑与:前一条成功才执行下一条
cd /tmp && echo "切换成功"

# 逻辑或:前一条失败才执行下一条
cd /nonexistent || echo "切换失败"

# 组合使用
cd /tmp && rm -f *.tmp || echo "清理失败"

# 花括号分组:在当前 Shell 中执行
{ echo "命令1"; echo "命令2"; } > output.txt

# 小括号分组:在子 Shell 中执行
(result="hello"; echo "$result")
# 外部无法访问 $result

2.7 业务场景:系统信息采集脚本

#!/bin/bash
# collect_info.sh —— 采集系统基础信息
set -euo pipefail

echo "==============================="
echo "  系统信息采集报告"
echo "  生成时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "==============================="

# 主机信息
readonly HOSTNAME=$(hostname)
readonly OS_VERSION=$(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d'"' -f2 || echo "未知")
readonly KERNEL=$(uname -r)
readonly UPTIME=$(uptime -p 2>/dev/null || uptime)

cat << EOF

[主机名]     $HOSTNAME
[操作系统]   $OS_VERSION
[内核版本]   $KERNEL
[运行时间]   $UPTIME

[CPU 信息]
$(lscpu 2>/dev/null | grep -E 'Model name|CPU\(s\)|Thread' | sed 's/^[[:space:]]*//' || echo "无法获取")

[内存使用]
$(free -h 2>/dev/null || echo "无法获取")

[磁盘使用]
$(df -h / /home 2>/dev/null || echo "无法获取")

[网络接口]
$(ip -4 addr show 2>/dev/null | grep -E 'inet ' | awk '{print $NF, $2}' || echo "无法获取")
EOF

2.8 扩展阅读