6

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.

6 Answers 6

8

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
0
4

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
0
2

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)

2

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
1

A nested find command will do the job in a single line, but this may appear a little quirky. ;-)

Specifically:

find . -name source -type d -exec sh -c 'find "$0" -type f -execdir mv "{""}"  .. \;' {} \;

Explanation:

  • find . -name source -type d finds all directories named source in the current (.) directory.

  • -exec sh -c '...' {} \; executes a sh with command '...' for every found directory, plugs the directory name into {}, and furthermore defines the executed sh's variable $0 with the contents of {} (see the option -c in the sh manpage)

  • the argument to sh -c, 'find "$0" -type f ... ' finds all files within the directory $0

  • and finally ... -execdir mv "{""}" .. moves the found files {} (here cloaked as "{""}" to prevent replacement due to the upstream -exec) to the (directory-local - therefore -execdir) parent directory ..

Example run:

marcus@gargabuntu:~/temp$ tree
.
├── sub1
│   └── source
│       ├── file1
│       └── file2
├── sub2
│   └── sub2.1
│       └── source
│           ├── something1
│           └── something2
└── sub3
    └── sub3.1
        └── sub3.1.1
            └── source
                └── other.zip

9 directories, 5 files
marcus@gargabuntu:~/temp$ find . -name source -type d -exec sh -c 'find "$0" -type f -execdir mv "{""}"  .. \;' {} \;
marcus@gargabuntu:~/temp$ tree
.
├── sub1
│   ├── file1
│   ├── file2
│   └── source
├── sub2
│   └── sub2.1
│       ├── something1
│       ├── something2
│       └── source
└── sub3
    └── sub3.1
        └── sub3.1.1
            ├── other.zip
            └── source

9 directories, 5 files
0

Simple one

dir='source'
for i in `find . -name  "*" | grep $dir/*| grep -v "$dir$"`; do
mv "$i" "${i%$dir/*}"
done

Test

$ cd Desktop/test/
/Desktop/test$ tree
.
├── sub1
│   └── source
│       ├── file1
│       └── file2
├── sub2
│   └── sub2.1
│       └── source
│           ├── something1
│           └── something2
└── sub3
    └── sub3.1
        └── sub3.1.1
            └── source
                └── other.zip

9 directories, 5 files
/Desktop/test$ dir='source'
/Desktop/test$ for i in `find . -name  "*" | grep $dir/*| grep -v "$dir$"`; do
>    mv "$i" "${i%$dir/*}"
> done
/Desktop/test$ tree
.
├── sub1
│   ├── file1
│   ├── file2
│   └── source
├── sub2
│   └── sub2.1
│       ├── something1
│       ├── something2
│       └── source
└── sub3
    └── sub3.1
        └── sub3.1.1
            ├── other.zip
            └── source

9 directories, 5 files
/Desktop/test$ 

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .