Linux 脚本安全实践:让一个小脚本可检查、可失败、可重跑

Linux 脚本安全实践:让一个小脚本可检查、可失败、可重跑
Perry一个好脚本不只是“这次运行成功”。它还应该告诉我们:处理了什么输入、在哪里写了结果、遇到异常是否停下,以及第二次运行会不会覆盖第一次的文件。
这篇从一个非常小的计数任务开始。脚本只在自己刚创建的临时目录里生成三行教学数据,不读取真实测序文件,不移动用户文件,也不自动删除任何目录。先建立这样的安全边界,再把思路迁移到正式工作流。
1. 先约定脚本的职责
我们的任务是读取两列教学 TSV:第一列样本名,第二列非负整数。结果包括有效样本数和计数之和。
输入范围有意很窄:样本名只能包含英文字母、数字、下划线或短横线;计数是十进制整数;两列都不能为空。它不是通用 CSV 解析器,也不是对真实表达矩阵进行生物学分析。
如果以后需要处理带引号、空字段、复杂注释或数十万列的数据,应该使用 Python/R 的表格读取工具,不能假定一个 read 循环可以处理所有格式。
正式任务还需要补充三个问题:如果某个样本失败,其他样本是否继续?哪些文件可以覆盖?结果怎么判定完整?这些属于分析策略,应当在写循环之前决定,而不是等错误发生后临时猜测。
2. 四个值得保留的习惯
路径变量通常放在双引号里,避免空格被拆成多个参数。需要把字符串原样写出时,使用 printf '%s\n' "$value",让格式串与数据分开。双引号与未引用变量的行为差别可查阅 Bash 的引用规则。
运行前明确输出目录。这里用 mktemp -d 创建新的目录,避免把“已有工作目录”误当成“本次练习目录”;不要用 mktemp -u 只猜一个名字。相关创建语义见 GNU Coreutils 的 mktemp 文档。
对预期内的失败显式处理,例如文件不存在或字段非法。对意外失败保留退出码和行号。只有“看到错误文字”不够:上游流程通常依据退出码决定是否继续。
尽量让每次运行产生独立结果。调试时保留失败现场,往往比写一个自动清理所有东西的退出钩子更有帮助。下面的程序结束后会打印练习目录,供你核对;它不会替你清理。
3. 完整练习:只在新的临时目录里工作
需要 Linux 或 WSL 中的 Bash 与 GNU Coreutils。请用编辑器将代码保存为 safe_counts.sh;保留 LF 换行,再执行 bash safe_counts.sh。不要把 bash 换成 sh:示例使用了 Bash 专属语法。
1 |
|
它特意创建了含空格的 input files 目录。如果去掉路径变量外侧的双引号,程序就可能把一个路径拆成两个参数。这种小范围练习比直接拿真实项目试错更容易理解引用规则。
读取时先保留整行,再用固定的正则约定两列,避免把连续制表符错误地压成一个字段分隔符。每条计数最多六位数字,只为避免把任意长的字符串直接送入 Bash 整数运算;shell 算术不是无限精度计算器。读取条件还保留了文件末尾没有换行时的非空记录,避免最后一行被悄悄漏掉。
4. 检查输出,而不是只看终端有没有报错
临时目录尾部的随机字符每次不同,但关键检查行应为:
1 | CHECK: samples=3 total=19 |
将终端打印的完整 Result 路径复制给 sed -n '1,5p',应看到下面两行。路径需要放在双引号里;不要凭记忆拼接另一个临时目录。
1 | samples total |
上面的展示用空格方便阅读,文件实际用制表符分列。日志结尾应该出现 SUCCESS、samples=3 与 total=19。有结果文件但没有成功标记时,不应把它默认为完整产物。
更重要的是检查输入:三条记录是否正是 S1、S2、S3?有没有意外多读了一行?本例只累计计数,不检测重复样本名;正式任务需要额外验证唯一性。明确知道程序尚未检查什么,本身就是安全实践的一部分。
5. set -e 与 pipefail 不是万能保险
-u 帮助发现未设置变量;pipefail 让管道中较早命令的非零退出也能影响整个管道状态;-e 在部分失败场景中终止脚本。但 if 条件、某些 &&/|| 链和其他上下文有例外,不能把它理解成“所有错误一定会立即停机”。具体条件应查 Bash 的 set 内建命令说明。
还要注意 head 提前退出的问题。一个长输出被 head -n 10 截断时,上游程序可能收到 SIGPIPE;打开 pipefail 后,整条管道可能呈现非零状态,即使你只是想看前十行。这不代表输入文件必然损坏,也不应该习惯性加上 || true 隐藏所有异常。
只预览文本文件,可以直接用 sed -n '1,10p' "$input_file";它不依靠另一个进程提前关闭管道。需要做“是否匹配”的判断时,把 grep 的退出状态放进明确的 if,区分未匹配与真正读取失败。对大型二进制数据或外部分析工具,先理解工具自身的退出码约定。
另一个小坑是 ((counter++)):当旧值是零时,算术命令的状态可能让 -e 触发。上面的脚本使用普通赋值 counter=$((counter + 1)),避免把计数的真假值混进错误处理。
6. 常见误区与练习
不要在未核对路径时批量重命名或删除;不要把任意文件名拼进一段 eval;不要一边读取原始文件,一边覆盖它;不要因为脚本有日志,就把未检查的结果称为可复现。
本例的 ERR 钩子提供诊断,不负责回滚已经写出的文件;它也不承诺覆盖所有信号与语法错误。可以先用 bash -n safe_counts.sh 做语法检查,但语法通过不等于逻辑正确。
完成后做三个小练习:把 S2 的计数改成 005,确认结果按十进制解释;在你自己的脚本副本里将一行计数改成 abc,确认程序拒绝输入;连续运行两次,核对两次目录不同且第一次的结果还在。
最后写下自己的检查清单:输入格式、样本唯一性、输出位置、覆盖策略、成功标记。把这些问题带到下一次真实分析里,比背下一长串 shell 命令更有用。


