How to move files from subdirectories that have the same directory name to its relative upper/parent directory?
A simple for
loop with the globstar
option enabled (run shopt -s globstar
to do that) will do the job:
dirname="source"
for i in ./**/"$dirname"/; do
mv "$i"* "${i%$dirname/}"
done
The GNU parallel
equivalent for this loop is:
parallel mv {}* {//}/ ::: ./**/"$dirname"/
A totally different approach is to use rename
to remove the “$dirname/” part from the string:
rename "s/$dirname\///" **/"$dirname"/*
Quoting with double quotes as I did here preserves the meaning of every special character except $
, `
, \
and !
. If you want to include “hidden” dot files in any of the above solutions, set the dotglob
option before running it: shopt -s dotglob
Explanation
globstar
If set, the pattern**
used in a pathname expansion context will match all files and zero or more directories and subdirectories. If the pattern is followed by a/
, only directories and subdirectories match.
./**/source/
matches every directory named source
in and under the current directory, the mv
command moves every file from inside the directory to its parent directory – ${i%source/}
is a parameter expansion which removes the string source/
from the end of the (path) string.
Example run
$ tree
.
├── sub1
│ └── source
│ ├── file1
│ └── file2
├── sub2
│ └── sub2.1
│ └── source
│ ├── something1
│ └── something2
└── sub3
└── sub3.1
└── sub3.1.1
└── source
└── other.zip
$ dirname="source"
$ for i in ./**/"$dirname"/; do mv "$i"* "${i%$dirname/}"; done
$ tree
.
├── sub1
│ ├── file1
│ ├── file2
│ └── source
├── sub2
│ └── sub2.1
│ ├── something1
│ ├── something2
│ └── source
└── sub3
└── sub3.1
└── sub3.1.1
├── other.zip
└── source
If you want a find
solution, you could use this:
find parent -name "source" -type d -exec bash -c 'cd "$1"; mv * ..' bash {} \;
Explanation:
-
find parent -name "source" -type d
- For each directory namedsource
inparent
... -
-exec bash -c '...' bash {} \;
- Call Bash withbash
as$0
and the directory path as$1
-
cd "$1"; mv * ..
- cd into the directory; move all its contents up one level.- Alternative:
mv "$1"/* "$1/.."
- Alternative:
This is more or less based on dessert's answer.
Including hidden files
find parent -name "source" -type d -exec bash -c 'shopt -s dotglob; cd "$1"; mv * ..' bash {} \;
-
shopt -s dotglob
- Make globs include hidden files
You can use the below code to get what you want:
dir=$(find . -name 'source' | sed s:source::)
for path in $dir; do
mv "$path"source/* "$path"
done
The find
command returns the directory path from parent to source directory. In this find . -name 'source'
'.' represents the parent directory and 'source' represents the subdirectory you want to find.
The sed
command removes source
from the result of find
command.
And the rest is just iteration (for
) and move command (mv
)
For these kinds of tasks where there might be surprises, one of the best ways is "script-a-script". We run a command which outputs a script, usually very repetitive, to do the task. Once we're satisfied it's correct, we pipe that through sh
to run it. This turns a complex problem into a much simpler edit problem, and it's an extremely general technique, applicable to all kinds of problems, not just this fiddly move-some-files-upstairs problem. It has the advantage of not using any exotic shell constructions, and so will work in every shell (pure Posix sh
, bash
, csh
, and so on). Because you see all the basic commands before they are executed, it's a good example of "look-before-you-leap".
First find all the directories we're going to modify:
$ find . -type d -name source
This gives
./sub3/sub3.1/sub3.1.1/source
./sub2/sub2.1/source
./sub1/source
Then for each of those directories we want to move the contents up one directory, we think of the command we'd run:
$ mv $dir/* $dir/..
We use awk
(or sed
or whatever) to write the commands. So we pipe the list of directories into awk:
$ find . -type d -name source \
| awk '{printf("mv %s/* %s/..\n", $0, $0);}'
mv ./sub3/sub3.1/sub3.1.1/source/* ./sub3/sub3.1/sub3.1.1/source/..
mv ./sub2/sub2.1/source/* ./sub2/sub2.1/source/..
mv ./sub1/source/* ./sub1/source/..
We can do this as many times as necessary, editing carefully, until we see the commands are correct.
Then we pipe all of that through sh
to actually do it:
$ find . -type d -name source \
| awk '{printf("mv %s/* %s/..\n", $0, $0);}' \
| sh
Often good to have the final output say what it's doing, and exit on first error, so use sh -e -x
:
$ find . -type d -name source \
| awk '{printf("mv %s/* %s/..\n", $0, $0);}' \
| sh -e -x
If you're not confident with awk
for this editing, you can do it with sed
or pure find
find . -type d -name source | sed 's|\(.*\)|mv \1/* \1/..|' # sed
find . -type d -name source -exec echo 'mv {}/* {}/..' ';' # find/echo