给脚本加三个安全阀:--dry-run、--yes 和退出码

举报
yd_251777447 发表于 2026/11/01 11:35:37 2026/11/01
【摘要】 写脚本的人迟早会遇到那个瞬间:手指刚从回车键上抬起来,就意识到路径写错了。 破坏性脚本的可怕之处不在于它会出错,而在于它 太快 。一个 rm rf 从敲下到执行完毕是毫秒级的,而人的反应时间是两百毫秒。等大脑发出"不对"的信号,命令已经跑完了。 所以凡是会删东西、改东西、发东西的脚本,都值得加三个安全阀。它们的实现成本都很低,加起来不到三十行。 一、 dry run:先看要做什

写脚本的人迟早会遇到那个瞬间:手指刚从回车键上抬起来,就意识到路径写错了。

破坏性脚本的可怕之处不在于它会出错,而在于它太快。一个 rm -rf 从敲下到执行完毕是毫秒级的,而人的反应时间是两百毫秒。等大脑发出"不对"的信号,命令已经跑完了。

所以凡是会删东西、改东西、发东西的脚本,都值得加三个安全阀。它们的实现成本都很低,加起来不到三十行。

一、--dry-run:先看要做什么,再决定做不做

核心思路很简单:把"决定要做什么"和"真的去做"拆开。

const DRY = process.argv.includes('--dry-run');
const plan = [];   // 收集操作

function planStep(desc, fn) {
  plan.push({ desc, fn });
}

// 收集阶段
for (const f of files) planStep(`删除 ${f}`, () => fs.unlinkSync(f));

// 执行阶段
for (const s of plan) {
  if (DRY) console.log('[dry-run]', s.desc);
  else { s.fn(); console.log('[done]', s.desc); }
}

if (DRY) console.log(`\n共 ${plan.length} 项操作,加 --yes 才会真正执行。`);

这个模式的好处是预览和执行共享同一份计划。如果分成两套代码(一套打印、一套执行),迟早会不一致——预览说删三个文件,实际删了五个,那预览就是骗人的。

一个真实的好处:--dry-run 会暴露你没想到的规模。你以为要处理十几个文件,跑一遍发现列出来两千个,那多半是 glob 写宽了。这种错误在不带 dry-run 的时候是看不出来的,因为你只会在删除完成后才发现。

顺手还能加个统计:

if (DRY) {
  const byType = {};
  plan.forEach(s => { const t = s.desc.split(' ')[0]; byType[t] = (byType[t] || 0) + 1; });
  console.table(byType);
}

二、--yes:让"确认"变成显式动作

有些脚本不适合 dry-run,比如发消息、提交表单这类一次性的外部操作。对这种,加一道确认:

const YES = process.argv.includes('--yes');

if (!YES) {
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  const ans = await new Promise(res => rl.question(
    `即将向 ${targets.length} 个目标发送,确认?(yes/N) `, res));
  rl.close();
  if (ans.trim().toLowerCase() !== 'yes') { console.log('已取消'); process.exit(0); }
}

三个细节值得一提:

  • 要输 yes 而不是直接回车。 回车是肌肉记忆,输单词需要真的想一下。
  • 在问题里带上数量。 "确认?"和"即将向 47 个目标发送,确认?"是完全不同的两句话,后者会让你停下来核一遍 47 这个数。
  • --yes 别设成默认。 一旦默认跳过确认,这个安全阀就形同虚设,因为人会习惯性地带上它。

三、退出码:让脚本能被别的程序正确判断

这条最容易被忽略,但它决定了脚本能不能被自动化串起来。

约定其实很固定:

| 码 | 含义 |

|---|---|

| 0 | 成功 |

| 1 | 运行失败(可预期的错误) |

| 2 | 用法错误(参数不对) |

常见的问题是"失败也返回 0"。比如:

try {
  await doWork();
} catch (e) {
  console.error('出错了:', e.message);
}
// 没写 process.exit(1),进程正常结束,退出码 0

这样在 shell 里 node task.js && deploy.sh 会继续往下跑,错误被吞掉,下游拿到的是残缺数据。

正确做法是明确区分:

try {
  await doWork();
} catch (e) {
  console.error('出错了:', e.message);
  process.exit(1);
}

参数校验失败用 2,跟运行失败区分开,调用方才能判断"是我的问题还是它的环境问题":

if (!srcFile) {
  console.error('用法: node task.js <文件> [--dry-run]');
  process.exit(2);
}

还有一个细节:process.exit() 会立刻终止进程,来不及写完 stdout。日志量大时可能出现最后几行丢失。稳妥的做法是先 flush:

process.exitCode = 1;   // 设退出码,让进程自然退出
// 或者
await new Promise(r => process.stdout.write('', r));
process.exit(1);

把 exitCode 当状态、让它自然退出,是最干净的写法。

四、三个阀怎么组合

一个完整的骨架大致长这样:

const args = process.argv.slice(2);
const DRY = args.includes('--dry-run');
const YES = args.includes('--yes');
const src  = args.find(a => !a.startsWith('--'));

if (!src) { console.error('用法: node task.js <路径> [--dry-run] [--yes]'); process.exit(2); }

const plan = [];
// ... 收集操作

if (!plan.length) { console.log('没有需要处理的内容'); process.exit(0); }

if (DRY) {
  plan.forEach(s => console.log('[dry-run]', s.desc));
  console.log(`共 ${plan.length} 项,未执行。`);
  process.exit(0);
}

if (!YES) {
  // 交互确认,拒绝则 exit(0)
}

for (const s of plan) { s.fn(); }
console.log(`完成 ${plan.length} 项`);

顺序上有个讲究:dry-run 优先于 yes。带 --dry-run 的时候不该弹确认,因为这时候根本不执行,弹确认只会让人困惑。

五、小结

三个阀,各管一件事:

  • --dry-run 让人先看清规模。它最大的价值不是"安全",是暴露你算错的量。
  • --yes 把"确认"从肌肉记忆变成显式意图,并且强制在提示里带上数量。
  • 退出码 让脚本能融入管道。失败返回 0 是最隐蔽的缺陷,因为它不会报错,只会让下游悄悄出错。

成本加起来不到三十行,收益是"再也不会因为手快而后悔"。

写脚本的时候不妨问自己一句:这个脚本如果参数写错了,最坏会怎样? 如果答案是"删掉不该删的东西"或者"把消息发给不该发的人",那就给它加阀。这三十行,比事后恢复数据便宜太多。


文中示例基于 Node.js 实测,适用于任何会修改外部状态的命令行脚本。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。