Linux: Bash 脚本基础——变量 / 条件 / 循环
将一系列 Linux 命令保存到文件,加上一些逻辑控制(条件、循环),就变成了一个 Shell 脚本。它是自动化一切重复工作的起点。
📋 前置知识:需要先掌握以下内容
- 第19课:压缩与归档
1. 你将学到
- Shebang 和执行权限
- 变量定义与引号区别
- 位置参数
- if/test 条件判断
- for/while 循环
2. 一个从手动部署到一键部署的故事
(1) 痛点:每天都要敲同样的 5 条命令
小明每天部署项目都要依次执行:git pull、npm install、npm run build、pm2 restart app、echo "done"。5 条命令,3 分钟,每天都在重复。
(2) 一个 8 行的脚本
他写了一个 deploy.sh:
#!/bin/bash
set -e
echo "开始部署..."
cd /var/www/myapp
git pull origin main
npm install
npm run build
pm2 restart app
echo "✅ 部署完成!"
set -e 是脚本安全的第一道防线——任何命令返回非零退出码时脚本立即停止。没有 set -e,脚本会继续执行后续命令,即使前面的命令已经失败,可能导致严重的连锁错误(如用破损的代码重启服务)。
(3) 收益:一键部署
从此小明只需执行 ./deploy.sh,所有步骤自动完成,节省时间、避免遗漏。
3. 知识点讲解
(1) Shebang
脚本的第一行必须是 Shebang——告诉系统用哪个解释器执行:
#!/bin/bash # 用 bash 执行
#!/bin/sh # 用 sh(兼容模式)
#!/usr/bin/env python3 # 用 Python 3
#!/bin/bash 是 Shebang——它告诉操作系统用哪个解释器执行脚本。没有这一行,系统会用当前 Shell 执行,可能导致兼容性问题(如 sh 不支持 [[ ]] 语法)。所有 bash 脚本都必须以 #!/bin/bash 开头。
(2) 变量
# 定义变量(等号两边不能有空格)
name="Alice"
age=25
# 使用变量(推荐用花括号包裹)
echo $name # Alice
echo ${name} # Alice(花括号更安全)
echo "你好, ${name}"
# 变量命名规则
# 1. 字母、数字、下划线组成
# 2. 不能以数字开头
# 3. 区分大小写(NAME != name)
三种引号的区别:
| 引号 | 示例 | 行为 |
|---|---|---|
| 双引号 | "Hello $name" |
解析变量和命令 |
| 单引号 | 'Hello $name' |
字面字符串,不解析 |
反引号/$() |
`date` 或 $(date) |
执行命令,取输出 |
ℹ️ 说明:单引号和双引号的核心区别——双引号中的
$变量和$(命令)会被解析替换,单引号中的一切都是字面量。需要拼接变量时用双引号,需要保留$等特殊字符时用单引号(如正则表达式、heredoc 中不想展开变量的场景)。
name="Alice"
echo "Hello $name" # Hello Alice
echo 'Hello $name' # Hello $name
echo "今天 $(date)" # 今天 Tue Jul 7 10:23:45 CST 2026
(3) 位置参数
./script.sh arg1 arg2 arg3
# $1 $2 $3
$0 # 脚本名本身
$1, $2 # 第 1、2 个参数
$# # 参数个数
$@ # 所有参数(分别作为独立参数)
$* # 所有参数(作为一个字符串)
$? # 上一个命令的退出码(0=成功,非0=失败)
$$ # 当前脚本的 PID
(4) 条件判断(if / test / [[ ]])
# test 命令 或 [ ]
if [ "$name" = "Alice" ]; then
echo "Hello Alice"
fi
# 更强大的 [[ ]](bash 特有)
if [[ "$name" == "Alice" ]]; then
echo "Hello Alice"
fi
# if-elif-else
if [ "$1" -gt 10 ]; then
echo "大于 10"
elif [ "$1" -gt 5 ]; then
echo "大于 5"
else
echo "小于等于 5"
fi
文件判断:
| 条件 | 含义 |
|---|---|
-f file |
是否为普通文件 |
-d dir |
是否为目录 |
-e path |
是否存在 |
-s file |
文件大小 > 0 |
-r file |
是否可读 |
-w file |
是否可写 |
-x file |
是否可执行 |
字符串比较:
| 条件 | 含义 |
|---|---|
$a = $b |
相等 |
$a != $b |
不等 |
-z $a |
字符串为空(长度 = 0) |
-n $a |
字符串不为空 |
数字比较:
| 条件 | 含义 |
|---|---|
$a -eq $b |
相等 |
$a -ne $b |
不等 |
$a -gt $b |
大于 |
$a -ge $b |
大于等于 |
$a -lt $b |
小于 |
$a -le $b |
小于等于 |
(5) 循环
# for 循环
for i in 1 2 3 4 5; do
echo "数字: $i"
done
for file in *.txt; do
echo "找到文件: $file"
done
for i in {1..10}; do
echo "$i"
done
# while 循环
count=1
while [ $count -le 5 ]; do
echo "第 $count 次"
count=$((count + 1))
done
# 读取文件逐行
while IFS= read -r line; do
echo "$line"
done < /etc/hostname
▶ 示例:基础脚本
#!/bin/bash
# hello.sh - 第一个 bash 脚本
echo "Hello, World!"
echo "当前用户: $(whoami)"
echo "当前目录: $(pwd)"
echo "文件数: $(ls | wc -l)"
输出:
TEXT 📖 仅展示Hello, World! 当前用户: alice 当前目录: /home/alice 文件数: 12
▶ 示例:使用位置参数
#!/bin/bash
# greet.sh - 向指定用户打招呼
if [ $# -eq 0 ]; then
echo "用法: $0 <用户名>"
exit 1
fi
echo "你好, $1!"
echo "欢迎使用 Linux 脚本。"
echo "脚本名: $0"
echo "参数数量: $#"
./greet.sh Alice
# 你好, Alice!
▶ 示例:if 条件判断
#!/bin/bash
# check-file.sh - 检查文件状态
FILE="${1:-test.txt}"
if [ ! -e "$FILE" ]; then
echo "文件 '$FILE' 不存在"
exit 1
fi
if [ -f "$FILE" ]; then
echo "类型: 普通文件"
elif [ -d "$FILE" ]; then
echo "类型: 目录"
fi
[ -r "$FILE" ] && echo "可读: ✅" || echo "可读: ❌"
[ -w "$FILE" ] && echo "可写: ✅" || echo "可写: ❌"
[ -x "$FILE" ] && echo "可执行: ✅" || echo "可执行: ❌"
▶ 示例:for 循环批量操作
#!/bin/bash
# rename.sh - 批量重命名 .txt 为 .md
count=0
for file in *.txt; do
if [ -f "$file" ]; then
mv "$file" "${file%.txt}.md"
echo "重命名: $file → ${file%.txt}.md"
count=$((count + 1))
fi
done
echo "总共重命名了 $count 个文件"
▶ 示例:while 循环与读取输入
#!/bin/bash
# countdown.sh - 倒计时
if [ $# -ne 1 ]; then
echo "用法: $0 <秒数>"
exit 1
fi
count=$1
while [ $count -gt 0 ]; do
echo "倒计时: $count"
count=$((count - 1))
sleep 1
done
echo "时间到!"
▶ 综合示例:批量文件处理脚本
#!/bin/bash
# batch-process.sh - 批量处理目录中的文件
DIR="${1:-.}"
ACTION="${2:-info}"
if [ ! -d "$DIR" ]; then
echo "错误: 目录 '$DIR' 不存在"
exit 1
fi
echo "处理目录: $DIR"
echo "操作: $ACTION"
echo "---"
case "$ACTION" in
info)
echo "文件统计:"
echo " 总文件数: $(find "$DIR" -type f | wc -l)"
echo " 总目录数: $(find "$DIR" -type d | wc -l)"
echo " 总大小: $(du -sh "$DIR" | cut -f1)"
;;
backup)
BACKUP="/tmp/backup-$(date +%Y%m%d)"
mkdir -p "$BACKUP"
cp -r "$DIR" "$BACKUP/"
echo "备份完成: $BACKUP"
;;
clean)
echo "清理 .log 文件..."
find "$DIR" -name "*.log" -delete
echo "清理 .tmp 文件..."
find "$DIR" -name "*.tmp" -delete
echo "清理完成"
;;
*)
echo "操作: info / backup / clean"
;;
esac
❓ 常见问题
Q: 脚本第一行的
#!/bin/bash必须写吗? A: 必须有。它告诉内核用哪个解释器来执行这个脚本。如果不写,默认用当前 Shell 执行(可能造成兼容性问题)。
Q: 为什么我写了脚本却提示
command not found? A: 两个原因:1)没有执行权限chmod +x script.sh。2)当前目录不在 PATH 中,需要用./script.sh而不是script.sh。
Q:
$@和$*有什么区别? A:$@将每个参数作为独立的"单词",$*将所有参数合并为一个字符串。大多数场景推荐用$@。遍历参数时for arg in "$@"; do是标准做法。
Q:
exit 0和exit 1有什么区别? A:exit 0表示成功,exit 非0表示失败(不同的数字表示不同的错误类型)。其他程序可以通过检查$?知道脚本是否成功执行。
Q: 脚本里怎么获取用户的输入? A: 用
read命令:read -p "请输入名字: " name,然后$name就是用户的输入。
📖 小节
- Shebang
#!/bin/bash、chmod +x赋予执行权限 - 变量
name="value"、用$name或${name}引用 - 双引号解析变量、单引号字面量、
$(cmd)执行命令 if [ 条件 ]条件判断、for i in 列表循环exit 0成功退出、exit 1错误退出
📝 作业
- 基础题(难度⭐):写一个脚本
info.sh,输出当前用户名、日期、当前目录和文件数量 - 进阶题(难度⭐⭐):写一个脚本
check-file.sh,接受一个文件名参数,检查文件是否存在并输出文件类型,然后写一个脚本backup-txt.sh,将当前目录所有.txt文件备份到/tmp/txt-backup/ - 挑战题(难度⭐⭐⭐):写一个倒计时脚本,接受秒数参数并逐秒倒计时,加上
set -e错误处理;然后用for循环创建 10 个以test-开头、编号结尾的目录,最后写一个脚本统计这 10 个目录的大小并排序输出