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)"

注意几个生产级脚本的标配:

8. 调试技巧

9. 何时该用 Shell,何时该用 Python?

Shell 脚本适合:

出现以下情况就该用 Python:

口诀:简单运维用 Shell,业务逻辑用 Python

小结

这一篇你学会了 Shell 脚本的基础:shebang、变量、参数、条件、循环、函数,以及 set -e set -u 等生产级写法。Shell 是 Linux 自动化的起点——掌握它,你就从"敲命令"升级到"让机器自己敲命令"。下一篇讲环境变量。

← 上一篇 Linux 软件包管理

下一篇 Linux 环境变量

✈️💬