Shell 脚本入门
把多条命令组合成一个可执行文件,就是 Shell 脚本。它是 Linux 自动化的最基础形式——备份、部署、定时任务、CI/CD 都离不开它。语法略奇葩,但简单够用,而且无处不在。
1. 第一个脚本:shebang 与运行方式
每个 Shell 脚本第一行都是 #!/bin/bash,叫 shebang(sharp-bang),告诉系统用哪个解释器运行这个文件。
#!/bin/bash
# 第一行叫 shebang,告诉系统用 /bin/bash 解释这个脚本
# 没有这行,系统可能用 sh 解释,语法略有不同
echo "Hello, Shell Script!"
# 保存为 hello.sh,然后两种运行方式:
# 1) bash hello.sh # 直接用 bash 运行,不用执行权限
# 2) chmod +x hello.sh # 给执行权限
# ./hello.sh # 当作程序运行(推荐)运行 Shell 脚本有两种方式:用 bash hello.sh 直接解释运行(不需要执行权限),或者 chmod +x hello.sh 后用 ./hello.sh 当作程序运行(推荐,更通用)。
2. 变量与输入
Shell 变量最大的坑是等号两边不能有空格——NAME=value 对,NAME = value 错(会被当成命令 NAME 带参数)。这是新手必踩的坑。
#!/bin/bash
# 变量:等号两边不能有空格!这是 Shell 最容易踩的坑
NAME=" aden "
AGE=28
PI=3.14
# 使用变量:前面加 $,推荐用花括号 ${VAR} 厘清边界
echo "Name: $NAME"
echo "Age: ${AGE} years old" # ${AGE} 比 $AGE 更明确
echo '单引号里 $NAME 不会展开' # 单引号是字面量
# 字符串操作
echo ${#NAME} # 字符串长度
echo ${NAME:0:3} # 截取前 3 个字符
# 命令替换:把命令的输出赋给变量
DATE=$(date +%Y-%m-%d) # $() 是新写法(推荐)
FILES=`ls *.txt` # 反引号是老写法(不推荐,不能嵌套)
echo "今天是 $DATE"
# 用户输入
echo -n "请输入你的名字: "
read USERNAME
echo "你好, $USERNAME!"
# 默认值
echo "CONFIG=${CONFIG:-/etc/myapp.conf}" # :- 表示变量为空时用默认值几个要点:用变量加 $;双引号会展开变量,单引号不会;$() 或反引号做命令替换;read 接受用户输入;\${VAR:-default} 给默认值。
3. 命令行参数
脚本可以接收命令行参数,通过特殊变量访问:
#!/bin/bash
# 脚本的命令行参数
echo "脚本名: $0" # $0 是脚本自身路径
echo "第一个参数: $1" # $1 ~ $9 是前 9 个参数
echo "第二个参数: $2"
echo "所有参数: $@" # $@ 是所有参数(数组风格)
echo "参数个数: $#" # $# 是参数数量
echo "上一条命令的退出码: $?" # 0 表示成功,非 0 表示失败
echo "当前进程 PID: $$"
# 运行示例:./args.sh alice 25 developer
# 脚本名: ./args.sh
# 第一个参数: alice
# 第二个参数: 25
# 所有参数: alice 25 developer
# 参数个数: 3最常用的是 $1(第一个参数)、$@(所有参数,数组风格)、$#(参数个数)、$?(上一条命令的退出码,0 表示成功)。脚本里用 exit 1 表示失败退出。
4. 条件判断:if 与 test
Shell 的 if 语法和大多数语言不太一样,要注意方括号 [ ] 两边必须有空格:
#!/bin/bash
# 条件判断:if [ ... ]; then ... fi
# 基本格式(注意 [] 两边的空格!)
if [ $AGE -ge 18 ]; then
echo "成年"
elif [ $AGE -ge 13 ]; then
echo "青少年"
else
echo "儿童"
fi
# 数字比较(-eq -ne -gt -ge -lt -le)
[ $a -eq $b ] # equal 等于
[ $a -ne $b ] # not equal 不等于
[ $a -gt $b ] # greater than 大于
[ $a -ge $b ] # greater or equal 大于等于
[ $a -lt $b ] # less than 小于
[ $a -le $b ] # less or equal 小于等于
# 字符串比较
[ "$s1" = "$s2" ] # 相等
[ "$s1" != "$s2" ] # 不等
[ -z "$s" ] # 字符串为空
[ -n "$s" ] # 字符串非空
# 文件判断
[ -f file.txt ] # 是普通文件
[ -d /tmp ] # 是目录
[ -e file.txt ] # 存在
[ -r file.txt ] # 可读
[ -w file.txt ] # 可写
[ -x file.txt ] # 可执行
# 推荐用 [[ ]] 双方括号(bash 特性,支持 && || 和模式匹配)
if [[ $NAME == "a*" ]]; then echo "以 a 开头"; fi记忆要点:数字比较用 -eq -gt 等(不是 == >);字符串比较用 =;文件判断用 -f -d -e。复杂逻辑推荐 [[ ]] 双方括号,支持 && ||。
5. 循环:for 与 while
#!/bin/bash
# for 循环:遍历列表
for f in *.txt; do
echo "处理 $f"
done
for i in 1 2 3 4 5; do
echo "数字 $i"
done
# 范围 {1..10} 和 seq
for i in {1..10}; do echo $i; done
for i in $(seq 1 5); do echo $i; done
# C 风格的 for
for ((i=0; i<5; i++)); do
echo "i = $i"
done
# 遍历命令输出
for line in $(cat users.txt); do
echo "用户: $line"
done
# while 循环:条件为真就继续
count=0
while [ $count -lt 5 ]; do
echo "count = $count"
count=$((count + 1)) # 算术展开 $((...))
done
# 逐行读文件(经典模式)
while IFS= read -r line; do
echo "行: $line"
done < data.txt
# until 循环:条件为假就继续(while 的反面)
until [ -f /tmp/ready ]; do
sleep 1
done高频模式:for f in *.txt 批量处理文件;while IFS= read -r line 逐行读文件(经典写法,处理带空格的行也安全);((i++)) 算术运算。
6. 函数
把重复逻辑封装成函数:
#!/bin/bash
# 函数:把重复逻辑封装起来
# 定义方式一
greet() {
echo "Hello, $1!"
}
greet "Alice" # 调用
# 定义方式二(等价)
function greet2() {
echo "Hi, $1"
}
# 带返回值的函数(返回的是退出码 0-255)
is_even() {
if [ $(($1 % 2)) -eq 0 ]; then
return 0 # 真(成功)
else
return 1 # 假(失败)
fi
}
# 用法:函数返回值用 $? 取
if is_even 4; then
echo "4 是偶数"
fi
# 想返回字符串,用命令替换
get_date() {
date +%Y%m%d
}
TODAY=$(get_date)
echo $TODAY
# 局部变量
counter() {
local n=0 # local:局部变量,不污染外部
n=$((n + 1))
echo $n
}关键:函数返回的是退出码(0-255),不是任意值——return 0 表示成功,非 0 表示失败。如果想"返回字符串",用命令替换 result=$(my_func)。函数内变量加 local 避免污染全局。
7. 实战:一个完整的备份脚本
把前面学的串起来,写一个真正有用的脚本:
#!/bin/bash
# 实战:一个完整的备份脚本 backup.sh
# 用法:./backup.sh 源目录 [目标目录]
set -e # 任何命令失败立刻退出(脚本必备)
set -u # 用未定义变量立刻报错
SRC="${1:-}" # 取第一个参数
DEST_DIR="${2:-/backup}" # 没传第二个参数就用默认值 /backup
# 参数校验
if [[ -z "$SRC" ]]; then
echo "用法: $0 <源目录> [目标目录]" >&2
exit 1
fi
if [[ ! -d "$SRC" ]]; then
echo "错误: $SRC 不是目录" >&2
exit 1
fi
# 变量
DATE=$(date +%Y%m%d-%H%M%S)
BASENAME=$(basename "$SRC")
ARCHIVE="$DEST_DIR/$BASENAME-$DATE.tar.gz"
# 执行
echo "开始备份 $SRC ..."
mkdir -p "$DEST_DIR"
tar -czf "$ARCHIVE" "$SRC"
# 结果
SIZE=$(du -h "$ARCHIVE" | cut -f1)
echo "✓ 完成: $ARCHIVE ($SIZE)"注意几个生产级脚本的标配:
set -e:任何命令失败立刻退出(默认 Shell 会继续往下跑,危险)。set -u:用未定义变量立刻报错(防止变量名打错引发事故)。- 参数校验和友好错误信息。
&1把错误信息写到 stderr,正常信息写到 stdout。
8. 调试技巧
bash -x script.sh:打印每一行命令执行过程(最常用的调试手段)。- 脚本里加
set -x开启、set +x关闭。 bash -n script.sh:只做语法检查,不执行。- 关键位置加
echo "DEBUG: var=$var"。
9. 何时该用 Shell,何时该用 Python?
Shell 脚本适合:
- 调用系统命令的组合(部署、备份、监控)。
- 处理文件、目录、管道。
- 简短的胶水代码(几十行以内)。
出现以下情况就该用 Python:
- 逻辑复杂,要算术、数据结构。
- 要处理 JSON、CSV 等结构化数据。
- 代码超过 100 行,Shell 的怪癖会拖慢你。
- 需要跨平台(Windows 上 bash 体验不好)。
口诀:简单运维用 Shell,业务逻辑用 Python。
小结
这一篇你学会了 Shell 脚本的基础:shebang、变量、参数、条件、循环、函数,以及 set -e set -u 等生产级写法。Shell 是 Linux 自动化的起点——掌握它,你就从"敲命令"升级到"让机器自己敲命令"。下一篇讲环境变量。
← 上一篇 Linux 软件包管理
下一篇 Linux 环境变量 →