Cannot move ./test to a subdirectory of itself, './test/test'

2

When trying to convert some files to lower case I noticed something which seems strange to me.

When executing this in bash:

find . -iname 'test' | while IFS='\n' read item; do mv $item $(echo $item | tr [:upper:] [:lower:]); done

And the source and target are unchanged for mv, I get the following error:

mv: cannot move '.test' to a subdirectory of itself, './test/test'

Where does the ./test/test come from? When echoing instead of executing mv, I get mv ./test ./test. I tested this both on FreeBSD 10 and Debian 8.2.0 with the same results. What am I missing?

Egon Olieux

Posted 2016-03-19T03:51:47.840

Reputation: 673

Alright, makes sense. What would you suggest if I want to rename a bunch of directories? Can this be done without errors without explicitly checking if they are the same? – Egon Olieux – 2016-03-19T05:13:05.467

I understand why moving to a different tree would work, but how is ./* different from .? ./* will expand to each item in the directory while . will take the current directory, but in the end it's still moving within the same directory. Let's take the directory dir for example, which contains 5 directories a, b, c, d and e. If I want to rename all subdirectories of dir, find . and find ./* will return the same result. – Egon Olieux – 2016-03-19T20:11:34.953

Assuming the current working directory is dir. – Egon Olieux – 2016-03-19T20:19:16.320

@FrankThomas +Egon: -iname test does not match .test; it's clear from the second result (with echo) that it's really ./test and just misquoted. -ilname is exactly the same as -iname except for symbolic links which are not involved here. ./* normally expands to all entries in . EXCEPT those beginning with . (dot), although some shells have an option to change this, in bash shopt -s dotglob. OTOH find . will always look at dotnames like ./.profile (other than . and ..), although here any such name doesn't match the -iname. – dave_thompson_085 – 2016-03-20T07:04:56.910

Answers

2

If the name is already in lower case, it will be given to the mv command twice, as you've seen.

If the last argument to mv is a directory, it will be treated as the destination dir into which all the other named entities should be moved.

mv test test will first check the last argument, whether it exists and, if so, if it's a directory. In this case it is, so mv will check whether it's the same mount point (if it isn't, i.e. the destination is a different device, it will need to copy the file then remove the original); in this case, it is. So mv constructs a sequence of rename(2) syscalls, appending all source names in turn to the directory specified as the destination: mv a b c d/ would generate rename("a", "d/a"), rename("b", "d/b"), and rename("c", "d/c") - so of course mv test test will try to call rename("test", "test/test");. Which is obviously an error, you can't move a directory inside itself.

To fix your problem, make the mv command conditional on the new name being different and not already existing (if you have two different files called "Test" and "test", you probably don't want to replace the second without asking). Without me having tested this, and typing it on a phone, it'll look something like this: new="$(echo "$old" | tr ...)"; [[ $new != $old && ! -e $new ]] && mv "$old" "$new" (watch the quotes, you don't want spaces in names to give you metaphorical wedgies).

Gabe

Posted 2016-03-19T03:51:47.840

Reputation: 1 837