How to move files from subdirectories that have the same directory name to its relative upper/parent directory?

5,768

Solution 1

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 named source in parent...
  • -exec bash -c '...' bash {} \; - Call Bash with bash 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/.."

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

Solution 2

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 Install 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

Solution 3

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

Solution 4

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)

Share:
5,768

Related videos on Youtube

Lukman Hakim
Author by

Lukman Hakim

Updated on September 18, 2022

Comments

  • Lukman Hakim
    Lukman Hakim almost 2 years

    So, I have a directory structure like this:

    parent/
    ├── sub1
    │   └── source
    │       └── file1
    │       └── file2
    ├── sub2
    │   └── sub2.1
    │       └── source
    │           └── something1
    │           └── something2
    └── sub3
        └── sub3.1
            └── sub3.1.1
                └── source
                    └── other.zip
    

    I want to move all files (with different filename) from all directories named source to its relative upper/parent directory. So, the result should be something like this:

    parent/
    ├── sub1
    │   ├── file1
    │   ├── file2
    │   └── source
    ├── sub2
    │   └── sub2.1
    │       ├── something1
    │       ├── something2
    │       └── source
    └── sub3
        └── sub3.1
            └── sub3.1.1
                ├── other.zip
                └── source
    

    Is there an easy way (one liner) to accomplish this, maybe using the find command? Preferably one that's not too complex for me to understand. :D I'm quite new to Linux.

    EDIT: I'm also going to make a bash script (so I can use it easily) out of the solution. For example: ./movefiles.sh myfolder So, preferably, the solution can easily accommodate, umm variables?, especially ones that have symbols like . (if it's a hidden directory), #, @, etc.