在Unix类型的操作系统(包括Linux、MacOS)中,cp、mv和rm是用于复制、移动和删除文件和目录的标准命令。-r、*和尾部斜线(文件路径后面是否带/)都在这些命令的操作中起到独特的作用,一旦用错了,就会导致各种问题。
本文尝试逐一解析这些命令选项的意义,以及一些经常容易出问题的用法。这些知识,平常或多或少有个大概的印象,这次做一个总结。
-r:递归选项对于cp:此选项允许递归地复制目录及其内容。如果尝试在没有-r的情况下复制目录,它会报错。所以cp在涉及目录时是一定需要-r的。
# 文件结构:
src/
├── file1.txt
├── file2.txt
# 执行命令:
cp -r src/ dest/
# 结果文件结构:
src/
├── file1.txt
├── file2.txt
dest/
├── file1.txt
├── file2.txt
对于mv:不需要-r选项,因为mv可以在没有它的情况下移动目录。
# 文件结构:
src/
├── file1.txt
# 执行命令:
mv src/file1.txt dest/
# 结果文件结构:
dest/
├── file1.txt
对于rm:使用-r选项可以递归地删除目录及其内容。在没有-r的情况下使用rm删除目录会报错。
*:通配符- 通用:代表任意数量的字符(包括零)。在文件操作的上下文中,它用于根据模式匹配多个文件或目录。例如,*.txt将匹配当前目录中的所有文本文件。实际上*由shell负责解释,扩展成具体的参数之后才会传递给rm等命令。
- 与-r结合使用:如果使用通配符和-r,它将对所有匹配的项执行操作,并深入到匹配的目录中。
rm *.txt 会被shell扩展成rm a.txt b.txt c.txt ...,所以rm等命令本身并不支持*选项。
尾部斜线 (/):指定目录- 在某些命令和上下文中,将尾部斜线添加到目录名称中可以强调该名称指的是目录而不是文件。在某些场景中这可能尤为重要。
- 对于cp和mv:当源目录有尾部斜线时,只有源目录的内容被复制或移动,而不是目录本身。没有尾部斜线,目录本身也包括在内。仔细对比下面两组命令,就能体会这一区别。
# 文件结构:
src/
├── file1.txt
# 执行命令:
cp -r src/ dest/
# 结果文件结构:
src/
├── file1.txt
dest/
├── file1.txt
# 文件结构:
src/
├── file1.txt
# 执行命令:(注意 src 后面没有 / )
cp -r src dest/
# 结果文件结构:
src/
├── file1.txt
dest/
├──src
├── file1.txt
- 对于rm:尾部斜线不会改变行为,因为rm不像cp和mv那样区分目录及其内容。
注意,这一行为并不稳定,我在有些操作系统(比如MacOS)上发现了这一行为,但是某些操作系统(比如Ubuntu)对源目录是否以/结尾并不敏感。
常见情况分析跨机器移动一种常见的情况是我们想用scp(类似cp)进行跨机器移动,把src目录挪到dest目录下,对应的命令是:
scp -r src server:dest
但是如果我们一不小心在src参数后面加了一个/,那情况就完全不一样了:
scp -r src/ server:dest
它并不会把src目录本身复制过去,只会把里面的内容复制过去。于是dest底下就多了一大堆文件。这种情况,就像是解压了一个压缩包但是没有新建一个文件夹存放解压文件、于是当前目录充满了解压后的文件,还无法撤销,不知道要删掉哪些文件……
(同样的,这个现象并不稳定,不同操作系统的实现不一样……)
建议不要使用下面这个命令,它可能漏掉一些*无法匹配的文件/文件夹,比如.git。
cp -r src/* dest
移动文件夹同时改名
如果想要把src目录挪到dest目录下,并重新命名为name,那么应该是mv src dest/name。注意,当src是一个文件时,不能使用mv src dest/name/,因为此时真正的完整路径其实是dest/name/src,而这个路径对应的目录并不存在。
总结使用rm/mv/cp/scp等命令的时候,涉及到-r选项、*通配符、路径是否以/结尾、目标路径是已有路径还是暂时不存在而需要创建的路径等等复杂情况。针对这些边界情况,有些操作系统的行为甚至可能不一致。
为了避免陷入类似的困惑,我们可以采用最保险的做法:
- 使用目录作为目标参数时,明确地在最后加上/作为结尾,表明我们是要移动到这个目录下,而不是要创建一个新的目录。
- 使用目录作为源参数时,用/表示要移动目录下的内容,不带/表示移动目录本身。
- 对于mv/cp/scp等兼具搬运文件并且重命名的功能,不要一次使用两个功能。请首先搬运到一个已有的文件夹下(保持文件/文件夹名不变),然后再对搬运完的文件/文件夹进行重命名操作。
就像解压一个压缩包时,我总是先新建一个临时文件夹,把压缩包放进去解压,然后再根据压缩包本身是否带目录结构来决定接下来如何移动这些文件,避免解压出来的文件污染了当前目录。
还有一些更复杂的情况,比如使用s3等对象存储时,也涉及到路径是否以/结尾、目标路径是已有路径还是暂时不存在而需要创建的路径等等复杂情况。这个就得看对象存储服务的提供商自定义工具的支持了。
对于这些容易出错的条件,我的建议是不要去学习这么多细节,而是尽量避免陷入到这些细节中,只使用语义最明确的用法,保持我们写出来的代码的“兼容性”与“可维护性”。
,