将来更改时,如何加固bash脚本以免造成损害?


46

因此,我删除了我的主文件夹(或更确切地说,是我拥有写权限的所有文件)。发生的是我有

build="build"
...
rm -rf "${build}/"*
...
<do other things with $build>

在bash脚本中,并在不再需要$build时删除了声明及其所有用法-而是rm。Bash高兴地扩展到rm -rf /*。是的

我感到很愚蠢,安装了备份,重新进行了丢失的工作。试图摆脱耻辱。

现在,我想知道:什么是编写bash脚本以使此类错误不会发生或者至少不太可能发生的技术?例如,我写过吗

FileUtils.rm_rf("#{build}/*")

在Ruby脚本中,解释器会抱怨build没有声明,因此该语言可以保护我。

我在bash中考虑过的内容,除了合计rm(相关问题中的许多答案都提到了,这并非没有问题):

  1. rm -rf "./${build}/"*
    那会杀死我目前的工作(一个Git回购),但没有别的。
  2. rm在当前目录之外执行操作时,需要进行交互的变体/参数化。(找不到任何内容。)类似的效果。

是这样吗,或者还有其他方法可以编写这种意义上的“健壮” bash脚本吗?


3
rm -rf "${build}/*无论引号在哪里,都没有理由。 rm -rf "${build} 因为会做同样的事情f
Monty Harder '18

1
使用shellcheck.net进行静态检查是一个非常可靠的起点。提供了编辑器集成,因此一旦删除仍在使用的内容的定义,您可能会从工具中收到警告。
查尔斯·达菲

@CharlesDuffy要让我感到羞耻,我在安装了BashSupport的IDEA风格的IDE中编写了该脚本,在这种情况下发出警告。是的,有效点,但是我真的需要硬取消。
拉斐尔

2
知道了 请务必注意BashFAQ#112中的警告。set -u虽然并不像set -e以前那样皱眉头,但是它仍然有它的陷阱。
查尔斯·达菲

2
开玩笑的回答:只要把#! /usr/bin/env ruby在每个shell脚本的顶部,而忘记了庆典;)
波德

Answers:


75
set -u

要么

set -o nounset

这会使当前的shell将未设置的变量的扩展视为错误:

$ unset build
$ set -u
$ rm -rf "$build"/*
bash: build: unbound variable

set -uset -o nounsetPOSIX shell选项

但是,不会触发错误。

为此,使用

$ rm -rf "${build:?Error, variable is empty or unset}"/*
bash: build: Error, variable is empty or unset

的扩展${variable:?word}将扩展为的值,variable除非它为空或未设置。如果为空或未设置,word则将在标准错误上显示,并且外壳程序会将扩展视为错误(该命令将不会执行,并且如果在非交互式外壳程序中运行,则会终止)。遗漏:只会针对未设置的值触发错误,就像under一样set -u

${variable:?word}POSIX参数扩展

除非set -e(或set -o errexit)也同样有效,否则这些都不会导致交互式外壳终止。${variable:?word}如果变量为空或未设置,则导致脚本退出。set -u如果与一起使用,将导致脚本退出set -e


至于第二个问题。没有方法限制rm不能在当前目录之外工作。

GNU的实现rm有一个--one-file-system选项,可以阻止它递归地删除已挂载的文件系统,但这与我所相信的非常接近,而无需将rm调用包装在实际检查参数的函数中。


附带说明: ${build}完全等同于,$build除非扩展是作为字符串的一部分发生的,其中紧随其后的字符是变量名中的有效字符,例如in "${build}x"


非常好谢谢!1)是${build:?msg}还是${build?msg}?2)在诸如构建脚本之类的情况下,我认为使用与rm不同的工具进行安全删除是可以的:我们知道我们不想在当前目录之外工作,因此我们通过使用受限来使其明确命令。一般而言,无需使RM安全。
拉斐尔

1
@Raphael抱歉,应该与:。现在,我将纠正该错字,稍后,当我回到计算机时,将提及它的重要性。
库萨兰达

2
@Raphael好的,我添加了一个简短的句子,说明没有会发生什么:(它只会对未设置的变量触发错误)。我不敢尝试编写一种在任何情况下都可以专门删除特定路径下的文件的工具。目前,解析路径和关心/不关心符号链接等对我来说有点太麻烦了。
库萨兰达

1
谢谢,解释有帮助!关于“本地rm”:我主要是在沉思,也许是为了“确定,那就是<toolname>”而钓鱼-但当然不是要帮助您吸血鬼写它!OO太好了,您已经提供了足够的帮助!:)
拉斐尔

2
@MateuszKonieczny,我想我不会,对不起。我认为,在需要时应使用此类安全措施。与使环境安全的一切事物一样,最终将使人们越来越粗心,并依赖于安全措施。最好了解每种安全措施的作用,然后根据需要有选择地应用它们。
库萨兰达

12

我将建议使用test/进行常规验证[ ]

如果您这样编写脚本,那将是安全的:

build="build"
...
[ -n "${build}" ] || exit 1
rm -rf "${build}/"*
...

[ -n "${build}" ]检查到"${build}"是一个非零的长度的字符串

||是在bash逻辑OR运算符。如果第一个命令失败,它将导致另一个命令运行。

这样,本来${build}就为空/未定义/等。该脚本将退出(返回码为1,这是一个通用错误)。

万一您删除了所有用途,这也将为您提供保护,${build}因为它[ -n "" ]始终是错误的。


使用test/ 的优点[ ]是还可以使用许多其他更有意义的检查。

例如:

[ -f FILE ] True if FILE exists and is a regular file.
[ -d FILE ] True if FILE exists and is a directory.
[ -O FILE ] True if FILE exists and is owned by the effective user ID.

不错,但是有点笨拙。我是否缺少某些东西,或者它与${variable:?word}@Kusalananda建议的功能几乎相同?
拉斐尔

@Raphael在“字符串是否具有值”的情况下也类似地工作,但是test(即[)具有许多其他相关的检查,例如-d(参数是目录),-f(参数是文件),-O(文件由当前用户拥有)等等。
Centimane

1
@Raphael同样,验证应该是任何代码/脚本的正常部分。另请注意,如果您删除了build的所有实例,您是否也将${build:?word}其删除了?的${variable:?word},如果你删除这个变量的所有实例格式不保护你。
Centimane

1
1)我认为确保假设成立(文件存在等)与检查是否设置了变量(编译器/解释器的imho工作)之间有区别。如果我必须手动处理后者,则最好使用超时髦的语法- if不需要成熟的语法。2)“您还不会同时删除$ {build:?word}吗?”-情况是我错过了变量的用法。使用${v:?w}本来可以保护我免受损害。如果我删除了所有用法,那么显然即使是简单访问也不会造成危害。
拉斐尔

综上所述,您的答案是对一般标题问题的公正而有益的回答:确保假设成立对存在的脚本重要。我想,问题正文中的具体问题最好由库萨兰南达回答。谢谢!
拉斐尔

4

在您的特定情况下,我过去对“删除”进行了重新处理以改为移动文件/目录(假设/ tmp与目录位于同一分区):

# mktemp -d is also a good, reliable choice
trashdir=/tmp/.trash-$USER/trash-`date`
mkdir -p "$trashdir"
...
mv "${build}"/* "$trashdir"
...

在幕后,这会将顶级文件/目录引用从源移动到$trashdir同一分区上的目标目录结构,而无需花费时间遍历目录结构并立即释放每个文件的磁盘块。在系统处于活动状态时,这会产生更快的清除速度,以换取稍慢的重新引导(/ tmp在重新引导时被清除)。

另外,对于要消耗大量磁盘空间的进程(例如,构建),要定期清理/tmp/.trash-$USER的cron条目将使/ tmp不再填满。如果您的目录与/ tmp在不同的分区上,则可以在该分区上创建类似/ tmp的目录,然后使用cron清除该目录。

但是,最重要的是,如果您以任何方式修改变量,则可以在清理发生之前恢复内容。


2
“这将在系统处于活动状态时产生更快的清理速度”-是吗?我本来以为两者都只是在更改受影响的inode。如果放在另一个分区上肯定不会更快/tmp(我认为,这对我来说总是如此,至少对于在用户空间中运行的脚本而言);然后,需要更改回收站文件夹(并且不会从的OS处理中获利/tmp)。
拉斐尔

1
您可以mktemp -d用来获取该临时目录,而不必担心其他进程的脚步(并正确地尊重$TMPDIR)。
Toby Speight

谢谢大家的补充,并提供了准确而有用的笔记。我想我最初发布此内容的速度太快,而没有考虑您提出的要点。
user117529

2

当变量未初始化时,使用bash参数替换指定默认值,例如:

rm -rf $ {变量:-“ /不存在”}


1

我总是尝试以一行开始我的Bash脚本#!/bin/bash -ue

-e的意思是“失败上第一uncathed é RROR”;

-u意思是“第一次使用失败的ü ndeclared变量”。

在出色的文章中找到更多详细信息使用非官方Bash严格模式(除非您放心调试)。作者也建议使用,set -o pipefail; IFS=$'\n\t'但出于我的目的,这太过分了。


1

检查变量是否已设置的一般建议是防止此类问题的有用工具。但是在这种情况下,有一个更简单的解决方案。

最有可能不需要$build为了删除它们而删除目录的内容,而不是实际$build目录本身。因此,如果您跳过无关的内容,*那么rm -rf /默认情况下,过去十年中大多数rm实现将拒绝执行一个未设置的值(除非使用GNU rm的--no-preserve-root选项禁用此保护)。

跳过拖尾/以及将导致rm '',这将导致错误消息:

rm: can't remove '': No such file or directory

即使您的rm命令未实现对的保护,此方法也有效/


-2

您正在用编程语言术语进行思考,但是bash是一种脚本语言:-)因此,对于完全不同的语言范例,请使用完全不同的指令范例。

在这种情况下:

rmdir ${build}

由于rmdir将拒绝删除非空目录,因此您需要先删除成员。你知道那些成员是什么吗?它们可能是脚本的参数,或者是从参数派生的,因此:

rm -rf ${build}/${parameter}
rmdir ${build}

现在,如果您在其中放置一些其他文件或目录(例如临时文件或您不应该使用的文件),rmdir则会引发错误。正确处理,然后:

rmdir ${build} || build_dir_not_empty "${build}"

这种思维方式对我很有帮助,因为……是的,去过那儿。


6
感谢您的努力,但这完全不可行。“您知道这些成员是什么”的假设是错误的;想一想编译器的输出。make clean将使用一些通配符(除非非常勤奋的编译器会创建它创建的每个文件的列表)。另外,在我看来,rm -rf ${build}/${parameter}仅将问题向下移动了一点。
拉斐尔

嗯 看看这是怎么读的。“谢谢你,但这是我在原始问题中没有透露的内容,因此,尽管它更适用于一般情况,但我不仅会拒绝您的回答,而且会否决它。” 哇。
Rich

“除了您在问题中所写的内容之外,让我做出其他假设,然后写出专门的答案。” *耸耸肩*(仅供参考,这不关您的事:没有拒绝投票。可以说我应该这样做,因为答案没有用处(至少对我而言)。)
拉斐尔

嗯 我没有做任何假设,并写了一个概括性的答案。make这个问题根本没有。编写仅适用于的答案make将是一个非常具体且令人惊讶的假设。我的答案适用于make除软件开发之外的其他情况,也不适用于通过删除内容而遇到的特定新手问题。
Rich

1
Kusalananda教我如何钓鱼。你走过去说:“既然生活在平原上,为什么不吃牛肉呢?”
拉斐尔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.