为什么 `cp` 被设计为静默覆盖现有文件?

Abs*_*cDo 30 cp history

cp使用以下命令进行了测试:

$ ls
first.html   second.html  third.html

$ cat first.html
first

$ cat second.html
second

$ cat third.html
third
Run Code Online (Sandbox Code Playgroud)

然后我复制first.htmlsecond.html

$ cp first.html second.html

$ cat second.html
first
Run Code Online (Sandbox Code Playgroud)

该文件second.html被无声地覆盖,没有任何错误。但是,如果我通过拖放具有相同名称的文件在桌面 GUI 中执行此操作,它将first1.html自动添加后缀。这可以避免意外覆盖现有文件。

为什么不cp遵循这种模式而不是默默地覆盖文件?

tel*_*coM 56

的默认覆盖行为cp在 POSIX 中指定。

  1. 如果 source_file 是普通文件类型,则执行以下步骤:

    3.a. 如果 dest_file 存在并且由上一步写入,则行为未指定。否则,如果 dest_file 存在,则执行以下步骤:

    3.ai 如果 -i 选项有效,cp 实用程序应向标准错误写入提示并从标准输入读取一行。如果响应不是肯定的,cp 将不再对 source_file 执行任何操作并继续处理任何剩余的文件。

    3.a.ii. dest_file 的文件描述符应通过执行与 POSIX.1-2017 的系统接口卷中定义的 open() 函数等效的操作来获得,该函数使用 dest_file 作为路径参数,以及 O_WRONLY 和 O_TRUNC 的按位包含 OR 作为oflag 论点。

    3.a.iii. 如果尝试获取文件描述符失败并且 -f 选项有效,则 cp 应尝试通过执行与使用 dest_file 调用的 POSIX.1-2017 系统接口卷中定义的 unlink() 函数等效的操作来删除文件作为路径参数。如果此尝试成功,cp 将继续执行步骤 3b。

在编写 POSIX 规范时,已经存在大量脚本,并内置了默认覆盖行为的假设。许多这些脚本被设计为在没有用户直接存在的情况下运行,例如作为 cron 作业或其他后台任务。改变行为会破坏他们。审查和修改它们以添加一个选项以在需要的地方强制覆盖可能被认为是一项艰巨的任务,但收益却微乎其微。

此外,Unix 命令行总是被设计为允许有经验的用户高效工作,即使是以初学者的艰苦学习曲线为代价。当用户输入一个命令时,计算机会期望用户是真正的意思,没有任何后顾之忧;用户有责任小心处理潜在的破坏性命令。

当最初的 Unix 被开发出来时,与现代计算机相比,系统的内存和大容量存储空间如此之少,以至于覆盖警告和提示可能被视为浪费和不必要的奢侈品。

在编写 POSIX 标准时,先例已经牢固确立,标准的编写者很清楚不破坏向后兼容性的优点。

此外,正如其他人所描述的,任何用户都可以通过使用 shell 别名,甚至通过构建替换cp命令并修改它们$PATH以在标准系统命令之前找到替换命令,为自己添加/启用这些功能,如果想要的。

但如果你这样做,你会发现你正在为自己制造危险。如果cp命令在交互使用时以一种方式运行,而在从脚本调用时以另一种方式运行,您可能不记得存在差异。在另一个系统上,您最终可能会粗心大意,因为您已经习惯了自己系统上的警告和提示。

如果脚本中的行为仍然符合 POSIX 标准,您可能会习惯于交互式使用中的提示,然后编写一个执行大量复制的脚本 - 然后发现您再次无意中覆盖了某些内容。

如果您也在脚本中强制执行提示,那么在没有用户的上下文(例如后台进程或 cron 作业)中运行时该命令会做什么?脚本会挂起、中止还是覆盖?

挂起或中止意味着本应自动完成的任务将无法完成。不覆盖有时本身也会导致问题:例如,它可能会导致旧数据被另一个系统处理两次,而不是被最新数据替换。

命令行的很大一部分力量来自这样一个事实,即一旦您知道如何在命令行上执行某些操作,您就会隐含地知道如何通过编写脚本使其自动发生。但只有当您以交互方式使用的命令在脚本上下文中调用时也完全相同时,这才是正确的。交互式使用和脚本化使用之间在行为上的任何显着差异都会造成一种认知失调,这会让高级用户感到恼火。

  • “为什么会这样?” “因为标准是这么说的。” “为什么标准这么说?” “因为它已经起作用了,就像这样。” (57认同)
  • 最后一段才是真正的原因。确认对话框和“_你真的想这样做吗?_”提示是给懦夫的:-) (16认同)
  • 最后一段就是为什么 `rm -rf` 如此有效,即使你实际上并不打算在你的主目录中运行它...... (2认同)
  • @TED 有趣的是,没有人提到 [unlink(2) 系统调用](https://www.freebsd.org/cgi/man.cgi?query=unlink&sektion=2) 也“未能”问 *“妈妈,可能我?”* 每当这些永恒的讨论再次浮出水面时,请予以确认。:) (2认同)

Ljm*_*art 21

cp来自Unix的开始。在编写 Posix 标准之前,它就已经存在了。事实上:Posix 刚刚正式确定了cp在这方面的现有行为。

我们谈论的是 Epoch (1970-01-01),当男人是真正的男人时,女人是真正的女人和毛茸茸的小动物......(我离题了)。在那些日子里,添加额外的代码会使程序变得更大。那是一个问题,因为第一台运行 Unix 的计算机是 PDP-7(可升级到 144KB RAM!)。所以事情很小,效率很高,没有安全功能。

所以,在那些日子里,你必须知道你在做什么,因为电脑没有能力阻止你做任何你后来后悔的事情。

(Zevar 有一部不错的漫画;搜索“zevar cerveauxassisse par ordinateur”以找到计算机的进化。或尝试http://a54.idata.over-blog.com/2/07/74/62/ dessins-et-bd/le-CAO-de-Zevar---reduc.jpg只要存在)

对于那些真正感兴趣的人(我在评论中看到了一些猜测):第cp一个 Unix 上的原始代码大约有两页汇编代码(C 后来出现)。相关部分是:

sys open; name1: 0; 0   " Open the input file
spa
  jmp error         " File open error
lac o17         " Why load 15 (017) into AC?
sys creat; name2: 0     " Create the output file
spa
  jmp error         " File create error
Run Code Online (Sandbox Code Playgroud)

(所以,很难sys creat

而且,当我们在做的时候:使用了 Unix 版本 2(代码片段)

mode = buf[2] & 037;
if((fnew = creat(argv[2],mode)) < 0){
    stat(argv[2], buf);
Run Code Online (Sandbox Code Playgroud)

这也是一个creat没有测试或保护措施的困难。请注意,V2 Unix 的 C 代码cp少于 55 行!

  • 几乎正确,除了“_small furry_”(来自半人马座阿尔法星的生物)而不是“_furry little_”! (5认同)
  • @sourcejedi:有趣!不会改变我的基本理论(只是无条件打开截断更容易,并且 `creat` 恰好相当于 `open`+`O_CREAT | O_TRUNC`),但是缺少 `O_EXCL` 确实解释了为什么它处理现有文件不会那么容易;尝试这样做本质上是不明智的(您基本上必须通过 `open`/`stat` 来检查存在,然后使用 `creat`,但是在大型共享系统上,当你使用 `creat` 时总是可能的,其他人制作了该文件,现在您无论如何都将其吹走了)。也可以无条件地覆盖。 (3认同)
  • @TED:“cp”的早期版本完全有可能只是用“O_CREAT | open”了目的地 O_TRUNC` 并执行了一个 `read`/`write` 循环;当然,现代的“cp”有很多旋钮,它基本上必须事先尝试“stat”目的地,并且可以轻松地首先检查是否存在(并且使用“cp -i”/“cp -n”),但如果期望是从原始的、简单的“cp”工具建立的,那么改变这种行为将不必要地破坏现有的脚本。毕竟,带有“alias”的现代 shell 不能仅仅使“cp -i”成为交互式使用的默认值。 (2认同)

xen*_*oid 18

因为这些命令也打算在脚本中使用,可能在没有任何人工监督的情况下运行,还因为在很多情况下您确实想要覆盖目标(Linux shell 的哲学是人类知道什么他/她正在做)

仍然有一些保护措施:

  • GNUcp有一个-n| --no-clobber选项
  • 如果您将多个文件复制到一个文件,cp则会抱怨最后一个文件不是目录。


T.E*_*.D. 10

"cp" 的设计可以追溯到 Unix 的原始设计。事实上,Unix 设计背后有一个连贯的哲学,它被半开玩笑地称为Worse-is-Better *略少。

基本思想是保持代码简单实际上是一个更重要的设计考虑,而不是拥有完美的界面或“做正确的事”。

  • 简单——设计必须简单,无论是在实现上还是在接口上。实现简单比接口更重要。简单性是设计中最重要的考虑因素。

  • 正确性——设计必须在所有可观察的方面都是正确的。简单比正确要好一些。

  • 一致性——设计不能过于不一致。在某些情况下,为了简单起见可以牺牲一致性,但最好放弃设计中处理不太常见情况的那些部分,而不是引入实现复杂性或不一致。

  • 完整性——设计必须涵盖尽可能多的重要情况。应涵盖所有合理预期的情况。可以为了任何其他质量而牺牲完整性。事实上,每当危及实现的简单性时,就必须牺牲完整性。如果保持简单,可以牺牲一致性来实现完整性;尤其没有价值的是界面的一致性。

强调我的

记住这是 1970 年,“我想在此文件不存在时才复制它”的用例对于执行复制的人来说是一个相当罕见的用例。如果这就是您想要的,您将能够在复制之前进行检查,甚至可以编写脚本。

至于为什么采用这种设计方法的操作系统恰好胜过当时正在构建的所有其他操作系统,这篇文章的作者也有一个理论。

越糟越好的哲学的另一个好处是程序员习惯于牺牲一些安全性、便利性和麻烦来获得良好的性能和适度的资源使用。使用新泽西方法编写的程序在小型机器和大型机器上都能很好地运行,并且代码将是可移植的,因为它是在病毒之上编写的。

重要的是要记住,初始病毒必须基本良好。如果是这样,只要它是便携式的,就可以保证病毒传播。一旦病毒传播,就会有改进它的压力,可能通过将其功能增加到接近 90%,但用户已经习惯于接受比正确的事情更糟糕的事情。因此,越差越好的软件首先会被接受,其次会让用户期望更少,第三会被改进到几乎正确的程度。

* - 或者作者,但没有其他人,称之为“新泽西方法”


sou*_*edi 9

是“一次做一件事”吗?

这个评论听起来像是一个关于一般设计原则的问题。通常,关于这些的问题是非常主观的,我们无法写出正确的答案。请注意,在这种情况下,我们可能会关闭问题。

有时我们对原始设计选择有解释,因为开发人员已经写过它们。但我对这个问题没有这么好的答案。

为什么cp这样设计?

问题是 Unix 已经有 40 多年的历史了。

如果您现在正在创建一个新系统,您可能会做出不同的设计选择。但是,如其他答案中所述,更改 Unix 会破坏现有脚本。

为什么 cp设计为静默覆盖现有文件?

简短的回答是“我不知道”:-)。

明白这cp只是一个问题。我认为没有任何原始命令程序可以防止覆盖或删除文件。shell 在重定向输出时也有类似的问题:

$ cat first.html > second.html
Run Code Online (Sandbox Code Playgroud)

此命令还会默默地覆盖second.html.

我有兴趣思考如何重新设计所有这些程序。它可能需要一些额外的复杂性。

我认为这是部分解释:早期的 Unix 强调简单的实现有关对此的更详细解释,请参阅本答案末尾链接的“越差越好”。

> second.html如果second.html已经存在,您可以更改以使其停止并出现错误。然而,正如我们提到的,有时用户确实想要替换现有文件。例如,她可能正在建立一个复杂的命令,尝试多次,直到它完成她想要的。

rm second.html如果需要,用户可以先运行。这可能是一个很好的妥协!它有其自身的一些可能的缺点。

  1. 用户必须键入文件名两次。
  2. 人们在使用rm. 所以我也想让它变得rm更安全。但是如何?如果我们rm显示每个文件名,并要求用户确认,她现在已经写3行一个命令,而不是。此外,如果她必须经常这样做,她会养成习惯,不假思索地键入“y”进行确认。所以这可能会很烦人,但仍然可能很危险。

在现代系统上,我建议安装trashcommand,并rm在可能的情况下使用它。Trash 存储的引入是一个好主意,例如对于单用户图形 PC

我认为了解原始 Unix 硬件的局限性也很重要——有限的 RAM 和磁盘空间、在慢速打印机上显示的输出 以及系统和开发软件。

请注意,原始 Unix 没有制表符完成功能,可以快速填写rm命令的文件名。(此外,原始的 Bourne shell 没有命令历史记录,例如当您在 中使用向上箭头键时bash)。

对于打印机输出,您将使用基于行的编辑器ed. 这比可视化文本编辑器更难学习。您必须打印一些当前行,决定如何更改它们,然后键入编辑命令。

使用> second.html有点像在行编辑器中使用命令。它的效果取决于当前状态。(如果second.html已经存在,其内容将被丢弃)。如果用户不确定当前状态,她应该运行lsls second.html首先运行。

“简单实现”作为设计原则

Unix 设计有一个流行的解释,它开始于:

设计必须简单,无论是实现还是接口。实现简单比接口更重要。简单性是设计中最重要的考虑因素。

...

Gabriel 认为“Worse is better”比麻省理工学院的方法产生了更成功的软件:只要初始程序基本上是好的,最初实施所需的时间和精力就会少得多,并且更容易适应新情况。例如,通过这种方式将软件移植到新机器上变得容易得多。因此,它的使用将迅速传播,早在[更好的]程序有机会被开发和部署之前(先发优势)。

https://en.wikipedia.org/wiki/Worse_is_better

  • @Kusalananda 数据丢失是一个问题。我个人对降低丢失数据的风险很感兴趣。对此有多种方法。说这是一个问题并不意味着替代方案也没有问题。 (2认同)
  • @riderdragon 用 C 语言编写的程序经常会以非常令人惊讶的方式失败,因为 C 信任程序员。但程序员并不那么可靠。我们必须编写非常先进的工具,例如[valgrind](https://en.wikipedia.org/wiki/Valgrind),需要这些工具来尝试发现程序员所犯的错误。我认为拥有像 Rust、Python 或 C# 这样的编程语言来尝试在不信任程序员的情况下强制执行“内存安全”是很重要的。(C语言是UNIX的作者之一创建的,目的是用一种可移植的语言编写UNIX)。 (2认同)
  • 更好的是 `cat first.html secondary.html &gt; first.html` 将导致 `first.html` 仅被 `second.html` 的内容覆盖。原始内容将永远丢失。 (2认同)