第五部分:函数与脚本

第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 后面必须跟一个文件名),然后它就会自动帮你处理好参数列表。

第三步:使用 getoptscase 搭建参数处理循环

我们来构建一个能接收 -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 命令的输出是给人看的,它的格式会变化,并且无法正确处理包含空格或换行符等特殊字符的文件名。在脚本中循环处理文件时,应该使用 findwhile 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 命令都会确保临时文件被删除。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐