第10课: 脚本编写(小白版)
第五部分:函数与脚本
第10课: 脚本编写
一. shebang (#!)
1. 核心概念:它是什么?
Shebang 是一个由井号和感叹号 #! 组成的特殊标记,它必须出现在脚本文件的第一行、第一列。
它的作用是告诉操作系统:“请不要用你默认的程序来运行我,而是用我后面指定的这个程序(解释器)来运行。”
好比说:Shebang 就像是一份文件的“封面说明”。当操作系统准备打开这份文件时,它首先会看这个封面说明。如果上面写着“请用bash程序打开”,系统就会调用bash来解释执行文件里的内容;如果写着“请用python程序打开”,系统就会调用python。
2. 一步一步构建你的 Shebang
第一步:最常见的 Shebang
在几乎所有的 Bash 脚本中,你都会看到这一行作为开头:
Bash
#!/bin/bash
-
#!:这就是 Shebang 标记。 -
/bin/bash:这是bash解释器程序在绝大多数 Linux 和 macOS 系统中的绝对路径。
第二步:理解它的重要性
如果你不写 Shebang,当你运行 ./myscript.sh 时,系统会使用你当前正在使用的 Shell 来尝试执行它。这可能会导致问题,因为你可能是用 zsh 或其他 Shell,而你的脚本里可能用了一些只有 bash 才支持的语法(比如 [[ ]])。
结论:写上 #!/bin/bash 可以确保你的脚本无论在何种环境下,都会被指定的 bash 解释器来执行,保证了一致性和可移植性。
第三步:更专业的 Shebang
在很多专业的脚本里,你会看到这样的写法:
Bash
#!/usr/bin/env bash
第四步:理解 env 的作用
这种写法的含义是:“请不要直接去 /bin/ 目录找 bash,而是使用 env 这个程序,去用户的系统 PATH 环境变量里,找到第一个被发现的 bash 程序,然后用它来执行脚本。”
-
为什么这样做更好? 因为在某些系统中,
bash可能被安装在/usr/local/bin/bash或其他非标准位置。#!/bin/bash这种“写死”的路径可能会找不到解释器而导致失败。而#!/usr/bin/env bash更加灵活和健壮,它能适应不同系统的环境配置。
小白建议:从现在开始,养成在你的所有脚本第一行都写 #!/usr/bin/env bash 的好习惯。
二. 脚本参数处理
一个好的脚本,应该像一个成熟的命令行工具一样,能够接收和处理用户传入的参数,并对错误的输入给出清晰的提示。
1. 基础:检查参数个数
这是任何需要参数的脚本都应该做的第一件事。我们使用特殊变量 $#。
第一步:创建一个函数来显示用法
为了代码整洁,我们把帮助信息封装在一个函数里。
Bash
function usage {
echo "用法: $(basename "$0") <输入文件> <输出目录>"
echo "这是一个示例脚本,用于演示参数处理。"
}
第二步:在脚本开头检查 $#
Bash
# 脚本期望收到2个参数
if [[ $# -ne 2 ]]; then
# 如果参数个数不对,就调用 usage 函数
usage
# 并以一个错误码退出
exit 1
fi
2. 进阶:处理带选项的参数 (-f, -v 等)
当脚本需要更复杂的参数,比如 -i input.txt -o output.dir -v 这种形式时,手动去解析 $1, $2 会非常混乱。这时,我们应该使用 Bash 内置的 getopts 命令。
好比说:getopts 就像一个专业的“参数解析机器人”。你告诉它你接受哪些选项字母(例如 i, o, v),以及哪些选项后面需要跟一个值(例如 -i 后面必须跟一个文件名),然后它就会自动帮你处理好参数列表。
第三步:使用 getopts 和 case 搭建参数处理循环
我们来构建一个能接收 -i <文件>,-o <目录> 和 -v (表示 verbose,详细模式) 的脚本。
Bash
# 初始化变量
VERBOSE=false
INPUT_FILE=""
OUTPUT_DIR=""
# --- getopts 循环 ---
# "i:o:vh" 是 “optstring”
# i: 表示 -i 选项后面需要一个参数
# o: 表示 -o 选项后面需要一个参数
# v 和 h 是开关选项,不需要参数
while getopts "i:o:vh" opt; do
case ${opt} in
i)
INPUT_FILE=$OPTARG # getopts 会把选项的参数值存放在 OPTARG 变量里
;;
o)
OUTPUT_DIR=$OPTARG
;;
v)
VERBOSE=true
;;
h)
usage # h 代表 help
exit 0
;;
\?) # 当遇到一个未定义的选项时
echo "错误: 无效的选项: -$OPTARG" >&2
exit 1
;;
:) # 当一个需要参数的选项没有提供参数时
echo "错误: 选项 -$OPTARG 需要一个参数。" >&2
exit 1
;;
esac
done
# --- 验证必要参数 ---
if [[ -z "${INPUT_FILE}" ]] || [[ -z "${OUTPUT_DIR}" ]]; then
echo "错误: -i 和 -o 选项是必须的。"
usage
exit 1
fi
这个结构是编写带选项的专业脚本的标准模板。
三. 调试技巧 (set -x, -e, -u)
编写脚本时,几乎不可避免会出错。学会调试是比编写代码本身更重要的技能。Bash 提供了几个强大的 set 命令,可以在脚本出错时提供大量线索。
1. set -x (执行跟踪)
-
作用: 开启“执行跟踪”模式。Shell 在执行每一行命令之前,会先把它打印到标准错误输出。
-
好比说:
set -x就像是给你的脚本安装一个“行车记录仪”,它会录下并播报脚本的每一个动作。这对于查看变量是否被正确替换,以及命令的执行流程非常有帮助。
第一步:在一个简单脚本中尝试
Bash
#!/bin/bash
set -x # 开启跟踪
NAME="Alice"
echo "Hello, ${NAME}"
set +x # 关闭跟踪
echo "跟踪已关闭。"
输出:
+ NAME=Alice
+ echo 'Hello, Alice'
Hello, Alice
+ set +x
跟踪已关闭。
你会看到带 + 号的行,这就是 set -x 打印出来的跟踪信息。
2. set -e (错误即退出)
-
作用: 开启“错误即退出”模式。脚本中任何一个命令的退出状态码 (
$?) 为非零时,脚本会立即终止执行。 -
好比说:
set -e就像是一条“安全生产红线”。流水线上任何一个环节出了问题,整条生产线立刻停工,防止带着错误继续生产,造成更大的损失。 -
为什么要用? 它可以防止脚本在某个关键命令(如
cd到一个目录)失败后,继续执行后续的危险命令(如rm -rf *)。
3. set -u (未定义变量即报错)
-
作用: 当脚本尝试使用一个未被定义的变量时,会将其视为一个错误,并立即终止执行。
-
好比说:
set -u是一个“仓库管理员”,他会检查你单据上写的每一个货品(变量)是否存在。如果你写错了一个名字,他会立刻拦住你,而不是给你一个空箱子。 -
为什么要用? 它可以帮你瞬间发现变量名的拼写错误。
Bash#!/bin/bash set -u MESSAGE="Hello" # 假设你不小心把 MESSAGE 拼成了 MESSGE echo "${MESSGE}" # 这行会导致脚本立即报错退出 echo "这一行将永远不会被执行。"
4. 终极武器:"Bash 严格模式"
在你的所有脚本开头,都推荐写上这样一行:
Bash
set -euo pipefail
-
set -e: 遇到错误立即退出。 -
set -u: 使用未定义变量立即退出。 -
set -o pipefail: 在一个管道命令中(如cmd1 | cmd2),只要有任何一个命令失败,整个管道的退出状态码就是失败的。 这行代码被称为“非官方的Bash严格模式”,它能让你的脚本变得极其稳健,能帮你避免无数潜在的错误。
四. 脚本安全考虑
当你的脚本需要处理用户输入或以高权限运行时,安全性就成了头等大事。一个不安全的脚本可能会导致数据被删除或系统被破坏。
1. 永远给变量引用加上双引号
这是最重要的安全规则。不加引号的变量扩展会导致分词和路径名扩展,这可能带来灾难性的后果。
第一步:理解风险 假设一个用户创建了一个包含恶意命令的文件名。
Bash
# 用户创建了一个邪恶的文件名
touch '; rm -rf ~'
# 你写了一个不安全的删除脚本
FILENAME="; rm -rf ~"
# rm $FILENAME <-- 致命错误!
当 Shell 执行 rm $FILENAME 时,它会把 $FILENAME 的内容进行分词,命令实际上变成了 rm ; rm -rf ~,这会删除你的整个家目录!
第二步:使用双引号来修复
Bash
# 正确、安全的写法
rm "$FILENAME"
现在,"$FILENAME" 会被当作一个单一的、完整的字符串,rm 命令只会尝试去删除一个名字很奇怪的文件 "; rm -rf ~",而不会执行后面的恶意命令。
2. 谨慎处理 rm -rf
在使用 rm 命令操作一个变量所指向的路径时,一定要先检查这个变量是否为空,或者是否意外地指向了根目录 /。
第一步:不安全的写法
Bash
# 如果 $TARGET_DIR 因为某种原因为空,这将变成 rm -rf /some/path/
# 如果 /some/path/ 是个软链接,后果不堪设想
rm -rf "/some/path/${TARGET_DIR}"
第二步:安全的写法 在执行删除前,增加严格的检查。
Bash
# 确保变量非空,并且不是根目录
if [[ -n "${TARGET_DIR}" && "${TARGET_DIR}" != "/" ]]; then
rm -rf "/some/path/${TARGET_DIR}"
echo "已删除目录 /some/path/${TARGET_DIR}"
else
echo "错误:目标目录无效或为空,删除操作已取消。"
fi
3. 不要解析 ls 的输出
ls 命令的输出是给人看的,它的格式会变化,并且无法正确处理包含空格或换行符等特殊字符的文件名。在脚本中循环处理文件时,应该使用 find 和 while read。
第一步:不安全的写法
Bash
# 如果文件名包含空格,这个循环就会出错
for filename in $(ls *.mp3); do ...
第二步:安全、专业的写法
Bash
# find ... -print0 会用一个空字符来分隔文件名,这是最安全的方式
# read -d '' 会以空字符为分隔符来读取
find . -name "*.mp3" -print0 | while IFS= read -d '' filename; do
echo "正在处理安全的文件名: ${filename}"
done
4. 安全地创建临时文件
不要自己构造临时文件名,比如 /tmp/myscript.log。这可能会被其他用户或程序预测到,导致安全问题(竞争条件)。
第一步:使用 mktemp 命令 mktemp 是专门用于安全地创建唯一临时文件或目录的命令。
Bash
# mktemp 会创建一个唯一的临时文件并返回其路径
TEMP_FILE=$(mktemp)
echo "临时文件已创建: ${TEMP_FILE}"
第二步:使用 trap 命令确保清理 一个专业的脚本,应该在退出时(无论是正常结束还是异常中断)自动清理它创建的临时文件。trap 命令可以帮你做到这一点。
Bash
# trap 命令会在脚本接收到指定信号时执行一段代码
# EXIT 信号表示脚本即将退出
trap 'echo "正在清理临时文件..."; rm -f "${TEMP_FILE}"' EXIT
# ... 脚本的主逻辑在这里 ...
echo "正在向临时文件写入内容..." > "${TEMP_FILE}"
现在,无论你的脚本如何结束,这个 trap 命令都会确保临时文件被删除。
更多推荐

所有评论(0)