In order to do what you want correctly using find
you have to understand the following concepts:
- The working directory of a process.
- The glob feature of bash (or other shells).
Every process in linux has a working directory as part of its internal state. You can imagine it as the "location" of the process in the filesystem. The shell is also a process and also has a working directory. The working directory of the shell is the directory (usually) displayed in your prompt.
When a process is created it will inherit the working directory from its parent. If you start a process from the shell then the shell is the parent. Every process can change its own working directory at will. You can change the working directory of the shell using the builtin command cd
.
The working directory is used when dealing with relative filenames. Relative filenames are filenames that do not start with /
. For example when you do cat foo
it will look for the file foo
in the current working directory, but when you do cat /tmp/foo
it will look for the file foo
in /tmp
.
When dealing with find -exec
you always have to think about the working directory. The working directory of -exec
is the working directory of find
. The working directory of find
is the working directory of the shell.
In your case:
find ./localFolders -name 'file' -type d -exec mv {}/* .. \;
The ..
will reference to the parent directory of the working directory of mv
which is the working directory of -exec
which is the the working directory of find
which is the working directory of the shell by the time you executed the command. That is not what you wanted.
There is an -execdir
which is like -exec
but it first switches the working directory to the directory where the matching item is found. -execdir
is a huge step towards the solution to your question, but there is another problem in your find
command: the *
a.k.a. glob.
A glob is something like a wildcard. For example foo*
will match foobar
and foobaz
(and foo
because *
also matches the null string). So far so good. The interesting part of globbing is that it is done by the shell, not by the executed command. When you execute rm foo*
then the shell will first expand the *
to foobar
and foobaz
and then execute rm foobar foobaz
. rm
never sees the *
.
What happens when there are no files starting with foo
in the current directory? Then the shell (usually) will not expand the *
and will pass the *
to the command in verbatim. That means rm foo*
will be executed as rm foo*
. rm
will look for a file named foo*
and it will (most likely) not find one and abort with an error. In case you are wondering: yes it is possible for a filename to contain *
, no don't try that at home.
In your case:
find ./localFolders -name 'file' -type d -exec mv {}/* .. \;
Before executing the find
command the shell will first try to expand the glob {}/*
. There are no files matching the pattern {}/*
so the glob will be passed verbatim.
Let's see what the find
command does. Suppose you are executing the find
command from /home/username/somedir
and there exists a directory /home/username/somedir/localFolders/foodir/file
with some files in it. find
will start searching in /home/username/somedir/localFolders
because it is told so in the parameters but the working directory of find
is /home/username/somedir
because it was executed from there.
Now find
found the directory /home/username/somedir/localFolders/foodir/file
. Before it executes the -exec
it will replace the {}
with the found item. That means mv {}/* ..
will become mv /home/username/somedir/localFolders/foodir/file/* ..
. mv
will fail because it will (most likely) not find a file named *
.
Suppose there is a file named *
then mv
will move the file to ..
. Remember that the working directory is /home/username/somedir
. That means mv
will move the file to /home/username/somedir/..
which is /home/username
. That is not what you wanted.
The "fix" for your find
command depends on several factors. For example:
- Are there "dotfiles" in the
file
directory? Dotfiles are files with names starting with a dot. The *
glob will (usually) not expand to filenames starting with a dot.
- Are there other directories in the
file
directory? find
will try to recurse in to them but fail because they have been moved.
- Are there directories named
file
in the file
directory? Where should those files go?
- Are there a gazillion files in the
file
directory? That might mess up the *
glob expansion.
- Are there filename collisions with files out of the
file
directory? Should they be overwritten?
- Are there spaces in the filenames? That will mess everything up.
To fix your find
command I will happily ignore all those factors.
Here is a fix using -execdir
and sh -c
to delay the expansion of *
:
find ./localFolders -name file -type d -execdir sh -c 'mv {}/* ..' \;
Since the *
is in quotes it will not get expanded ahead of time. When -execdir
executes the command then sh -c
will expand the *
in the working directory.
Here is another fix without sh -c
:
find ./localFolders -path "*/file/*" -execdir mv {} .. \;
I used -path
to find all items under directories with the name file
. The *
are in quotes so they will not get expanded ahead of time. This time find
takes care of the interpretation of the *
. Note that this will also match something like ./localFolders/foodir/file/bardir/file/somefile
. As said I happily ignored that factor.
And for the sake of completeness another fix without -execdir
:
find ./localFolders -name file -type d -exec sh -c 'mv {}/* {}/..' \;