有几种方法可以解释“用另一个文件替换一个文件”的操作,但这里我要重点介绍的是可以通过命令实现的方法
mv /that/there/someotherfile /this/here/somefile
在此示例中/that/there/someotherfile
, 和/this/here/somefile
应该是文件系统中当前存在的常规文件。
如果一切顺利,运行上面的命令后,“以前称为”的文件/that/there/someotherfile
将消失,但它的内容现在将是该文件的新内容/this/here/somefile
。后者之前的内容将被“覆盖”。
现在考虑“用一个目录替换另一个目录”的类似操作。例如,/path/to/targetdir
用目录覆盖某些目录/some/other/path/to/sourcedir
。我可以这样做
rm -rf /path/to/targetdir && mv /some/other/path/to/sourcedir /path/to/targetdir
我可以用一个有效的“或多或少标准” 1命令来完成此操作吗无论相关两个目录的内容如何?
我知道,如果/path/to/targetdir
碰巧是一个空目录,那么
mv -T /some/other/path/to/sourcedir /path/to/targetdir
...将完成这项工作。
另外,我知道如果/path/to/targetdir
不包含任何不存在于 下的相对路径/some/other/path/to/sourcedir
,并且两个目录下存在的所有相对路径都指向相同类型的文件系统项(即它们都是目录,或都是常规文件等)。 ),则可得以下关闭到上述操作
rsync -a --remove-source-files /some/other/path/to/sourcedir/ /path/to/targetdir
当然,实现2 个脚本或函数来封装上面给出的rm -rf
+序列并不困难mv
,但我想避免实现已经通过或多或少标准 Unix 命令可用的东西。
1我意识到这个问题的答案关键取决于人们对允许的命令集的看法,不幸的是,在这里我只能提供比有力的挥手更好的东西......例如,我认为cp
和mv
是“更多或-less 标准”,但即使在这种情况下,这些命令所采用的某些选项也可能不是。事实上,如果使这一条件足够精确(例如,将允许的命令限制为“强制 POSIX 直到”),在上述意义上,可能没有使用单个命令“用另一个目录替换一个目录”的通用方法。如果是这样,请随意定义允许的命令集,以使您的命令足够有用和/或有趣。换句话说,在选择允许的命令集时,我准备遵循您的好品味。
2著名的遗言。
答案1
如果您忘记了rsync
通常的可重新启动性,这个极其危险的命令可能会起作用
rsync -av --remove-source-files --delete-before /path/to/source/ /path/to/target
但有一些警告,
- 它不会移动文件 - 它会复制文件并删除原始文件。对于同一文件系统上的大文件,这可能是一个问题
- 它不会删除源目录。如果这是一个问题,您将返回到两个命令,此时您也可以恢复到原来的
rm && mv
构造 - 这不是 POSIX
总的来说,我认为我更喜欢这种rm && mv
方法。这个版本需要bash
(或者可能有一些其他具有数组的 shell)。我相信它的使用mv
,rm
和find
都符合 POSIX 标准。
rmmv() {
local args=("$@") target
if [ $# -eq 0 ]
then
echo "${0##*/}: missing file operand" >&2
exit 1
elif [ $# -eq 1 ]
then
echo "${0##*/}: missing file operand after '${args[0]}'" >&2
exit 1
fi
target="${args[@]: -1}"
unset "args[${#args[@]}-1]"
if [ -d "$target" ]
then
# Directory target; remove its contents
( cd -P -- "$target" && find . -depth -path './*' -exec rm -rf {} + )
elif [ "${#args[@]}" -gt 1 ]
then
# Multiple sources but not a directory
echo "${0##*/}: target '$target' is not a directory" >&2
exit 2
fi
# Do it
mv -- "${args[@]}" "$target"
}
答案2
TL,DR:使用符号链接。
POSIX 目录
仅使用 POSIX 系统调用,不可能自动替换非空目录。这rename
系统调用仅允许目标为空目录。这link
系统调用禁止目标为目录。
/some/other/path/to/sourcedir
为了替代/path/to/targetdir
,有一些非原子方法。在整个答案中,我假设目标目录已经存在;如果没有,您可能需要单独处理这种情况,这在并发情况下可能会有点棘手。我还假设源和目标位于同一文件系统上。
将现有目标移开,然后将新版本移到位。这会留下一小段时间窗口,在此期间目标不存在。如果两个替换进程同时工作,则此方法无法可靠地工作。
mv -f /path/to/targetdir /path/to/targetdir.old mv /some/other/path/to/sourcedir /path/to/targetdir rm -rf /path/to/targetdir.old
删除现有目标,然后将新版本移动到位。这留下了一个潜在的大时间窗口,在此期间目标仅部分填充,并且在较小的时间窗口期间目标不存在。如果两个替换进程同时工作,则此方法无法可靠地工作。
rm -rf /path/to/targetdir mv /some/other/path/to/sourcedir /path/to/targetdir
清空现有目标,然后将新版本移动到位。这留下了一个潜在的很长的时间窗口,在此期间目标仅被部分填充。目标窗口保持存在,并且以原子方式填充。如果两个替换进程同时工作,则此方法无法可靠地工作。
(cd /path/to/targetdir && rm -rf ..?* .[!.]* *) mv /some/other/path/to/sourcedir /path/to/tmp/targetdir mv /path/to/tmp/targetdir /path/to/
请注意中间步骤,其中新版本具有与目标相同的基本名称。这是必要的
mv
:如果传递到的目标mv
是现有目录,mv
则将源移动到其中。因此,我们必须将传递的目标设置为mv
我们要覆盖的现有目录的父目录。
如果您愿意使用特定于内核的系统调用,可能有一些方法可以原子地进行替换。
Linux目录
系统调用renameat2
Linux ≥3.15 可以原子地交换任意类型的两个目录条目。我不知道有任何实用程序为其提供接口。
perl -we '
require "syscall.ph";
sub AT_FDCWD() {return -100}
sub RENAME_EXCHANGE () {return 2}
my ($source, $target) = @ARGV;
$! = 0;
syscall(SYS_renameat2(), AT_FDCWD, $source, AT_FDCWD, $target, 2) != -1 or die $!}
' /some/other/path/to/sourcedir /path/to/targetdir &&
rm -rf /some/other/path/to/sourcedir # This now contains the former target
这是完全原子的替换。如果多个替换进程同时执行此操作,最终结果是其中一个新版本将被放置到位,而其他版本将被删除。
另一种有效地原子地替换目录的方法是绑定挂载。许多 Unix 变体都存在此功能,但使用方式可能有所不同。这里我将使用Linux的mount --bind
,它需要root权限。另一个值得注意的方法是 FUSE 文件系统bindfs
,它不需要不寻常的权限。
mkdir /path/to/targetdir.old
mount --bind /path/to/targetdir /path/to/targetdir.old
mount --bind/some/other/path/to/sourcedir /path/to/targetdir
rm -rf /path/to/targetdir.old
请注意,这不会移动文件,只是使它们在目标路径下可见。通过这种方法,源头仍然存在。
POSIX 符号链接
进行原子替换的方法是通过符号链接。符号链接能被原子替换。这有点棘手,因为与直觉相反,symlink
不会做的。你必须使用rename
。此外,如果mv
给定一个目标,它是目录的符号链接,它将移动源在下面该目录,而不是覆盖符号链接。您可以mv
通过将包含符号链接的目录作为目标传递来覆盖作为目录符号链接的目标,这要求源具有相同的基本名称。
# Precondition: /path/to/targetdir doesn't exist or is a symbolic link.
mkdir -p staging
ln -s /some/other/path/to/sourcedir staging/targetdir
old_target=$(cd /path/to/targetdir && pwd -P)
mv staging/targetdir /path/to/targetdir
# Postcondition: /path/to/targetdir is a symbolic link to the new version.
rm -rf "$old_target"
如果有多个并发实例,这将无法可靠地工作,因为重命名是原子性的,但弄清楚要删除的内容却不是。
答案3
在这种情况下,我建议添加一个命令,而不是删除命令:
mv target somewhere_else
mv source target
如果源、目标和其他地方都在同一个文件系统中,那么这两个命令应该很快返回。然后,不一定马上:
rm -rf somewhere_else
如果磁盘空间有问题,您可以将上述任务划分为子树。
source
根据通常在和之间变化的文件/文件夹的类型和数量,可以找到更好的解决方案target
:rsync
,递归diff/patch
,版本控制,...