mv操作非常简单,但是在实际时这么简单的命令可以变得非常奇怪,这篇文章以mv *的命令来看一下可能会出现的几种有意思的事情,同时进行一些扩展的思考。
以前曾经碰到过的一个问题,现象中就包括mv命令执行出错,说是第一次成功,第二次出错,后来通过系统日志和审计日志查询到执行的命令是mv *,实际上是不懂技术的操作人员执行时命令考漏了后面的第2个参数。
现象重现这个示例主要想说明的是需要注意执行时的现场,因为执行的动作可能会对现场进行破坏,确认执行时点的当时状态是非常重要的,比如在一个空目录下,我们执行这条命令会发现mv * 是出错的
liumiaocn:testmv liumiao$ ls liumiaocn:testmv liumiao$ mv * usage: mv [-f | -i | -n] [-v] source target mv [-f | -i | -n] [-v] source ... directory liumiaocn:testmv liumiao$
*在执行的时候会被展开,由于当前目录下没有任何文件,而mv需要两个参数,自然会提示使用上的错误信息,我们将*展开的信息设定为两个文件,就会满足mv命令执行时所需要的条件,比如进行如下准备:
liumiaocn:testmv liumiao$ echo "hello " >file1 liumiaocn:testmv liumiao$ echo "liumiao " >file2 liumiaocn:testmv liumiao$ ls file1 file2 liumiaocn:testmv liumiao$
然后执行mv *,结果如下所示:
liumiaocn:testmv liumiao$ ls file1 file2 liumiaocn:testmv liumiao$ liumiaocn:testmv liumiao$ mv * liumiaocn:testmv liumiao$
结果是什么呢:
liumiaocn:testmv liumiao$ ls file2 liumiaocn:testmv liumiao$ cat file2 hello liumiaocn:testmv liumiao$
实际上mv *执行的操作是mv file1 file2,所以从结果上来看完全一致,这种小问题说穿了没有一点技术含量,而且其执行的前提非常严格,比如*展开是一个文件,或者多余两个文件都无法成功:
liumiaocn:testmv liumiao$ touch file1 liumiaocn:testmv liumiao$ ls file1 liumiaocn:testmv liumiao$ mv * usage: mv [-f | -i | -n] [-v] source target mv [-f | -i | -n] [-v] source ... directory liumiaocn:testmv liumiao$ liumiaocn:testmv liumiao$ touch file2 file3 liumiaocn:testmv liumiao$ ls file1 file2 file3 liumiaocn:testmv liumiao$ mv * usage: mv [-f | -i | -n] [-v] source target mv [-f | -i | -n] [-v] source ... directory liumiaocn:testmv liumiao$ ls file1 file2 file3 liumiaocn:testmv liumiao$扩展思考示例
很多人会说这么简单的拷贝缺参数的问题,肯定不是我。那我们把这条mv *稍微改一下:
示例代码:mv * ${FILE_DEST_DIR}
在执行过程中,程序运行的某种异常情况或者分支中导致了FILE_DEST_DIR变量为空的情况呢?结果很有会导致最初的判断走上歧途,浪费了不必要的根因分析的时间。
解决方式 事前详细的判断其实即可解决这个问题,比如确实保证此目录存在的前提下才进行mv操作,写脚本的时候也要像写其他高级语言一样考虑所有的情况,对有经验的开发者来说不是问题,但是很多不是专门写脚本的开发者经常会碰到这个问题,另外还有脚本的一个使用场景就是“一行语句”,在一行语句中写详细和复杂的逻辑分支非常不现实,但是又需要的情况下怎么办是需要进一步思考的
扩展场景示例1当然mv * 这样简单的示例一眼就能看出问题所在,这里我们再使用一个一行的语句把这个情况变得稍微复杂,来看看有趣的事情,首先做如下事前准备
liumiaocn:testmv liumiao$ mkdir -p dir1 dir2 dir3 liumiaocn:testmv liumiao$ touch dir1/file dir2/fie2 dir3/file liumiaocn:testmv liumiao$ echo "hello" >dir1/file liumiaocn:testmv liumiao$ echo "liumiao" >dir3/file liumiaocn:testmv liumiao$ cat dir1/file hello liumiaocn:testmv liumiao$ cat dir3/file liumiao liumiaocn:testmv liumiao$
然后将当前目录下的文件全部拷贝到/tmp下新建的一个名为testdestmv目录中,执行日志如下所示:
liumiaocn:testmv liumiao$ mkdir -p /tmp/testdestmv liumiaocn:testmv liumiao$ ls /tmp/testdestmv liumiaocn:testmv liumiao$ liumiaocn:testmv liumiao$ find . -type f ./dir2/fie2 ./dir3/file ./dir1/file liumiaocn:testmv liumiao$ liumiaocn:testmv liumiao$ find . -type f |xargs -I @ mv @ /tmp/testdestmv/ liumiaocn:testmv liumiao$
结果如下:
liumiaocn:testmv liumiao$ find . -type f |xargs -I @ mv @ /tmp/testdestmv/ liumiaocn:testmv liumiao$ ls /tmp/testdestmv/ fie2 file liumiaocn:testmv liumiao$ cat /tmp/testdestmv/file hello liumiaocn:testmv liumiao$
结合前面所讲的内容也可以理解,但是我们需要思考的是,如果代码中无法定位现象,这样一行语句的问题分析将会是非常麻烦的事情,这些是需要进一步考虑的。
扩展场景示例2我们这里将上述扩展场景示例1和*的使用进一步结合,准备如下内容
liumiaocn:testmv liumiao$ echo "hello" >dir1/file liumiaocn:testmv liumiao$ echo "liumiao" >dir3/file liumiaocn:testmv liumiao$ tree . . ├── dir1 │ └── file ├── dir2 └── dir3 └── file 3 directories, 2 files liumiaocn:testmv liumiao$
执行结果如下所示:
liumiaocn:testmv liumiao$ find . -type f |xargs mv liumiaocn:testmv liumiao$ find . -type f ./dir1/file liumiaocn:testmv liumiao$ cat dir1/file liumiao liumiaocn:testmv liumiao$总结
从结果去理解非常容易,但是如果我们没有意识到不同文件夹下有相同的文件,同时在实际代码编写中又有变量赋值为空的异常分支的情况,综合起来的问题有可能会让我们觉得非常奇怪,而这些只需要我们更加规范的编码,对于异常系的更好控制,一般都能够解决,但是在很多既有的项目代码中可能已经存在类似的代码,这些是我们重构时需要多加注意的。