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 | 使用系统默认 Shell | POSIX 兼容脚本 |
#!/usr/bin/env python3 | Python 脚本 | 混合项目中的 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