8

I have this folder structure:

├── foo1
│   ├── bar1.txt
│   └── bar2.txt
├── foo2
│   ├── bar3.txt
│   └── bar4 with a space.txt
└── foo3
    └── qux1
        ├── bar5.txt
        └── bar6.txt

that I would like to flatten into this, with an underscore between each folder level:

├── foo1_bar1.txt
├── foo1_bar2.txt
├── foo2_bar3.txt
├── foo2_bar4 with a space.txt
├── foo3_qux1.bar6.txt
└── foo3_qux1_bar5.txt

I've looked around and I haven't found any solution that work, mostly I think because my problem has two particularities: there might be more than one folder level inside the root one and also because some files might have spaces.

Any idea how to accomplish this in bash? Thanks!

Edit: Running gleen jackman proposed answer I get this:

enter image description here

There are two underscores for the first level folder. Any idea how to either avoid this or just rename it so that is just one underscore? Thanks.

13
find */ -type f -exec bash -c 'file=${1#./}; echo mv "$file" "${file//\//_}"' _ '{}' \;

remove echo if you're satisfied it's working. Don't worry that the echo'ed commands don't show quotes, the script will handle files with spaces properly.

If you want to remove the now empty subdirectories:

find */ -depth -type d -exec echo rmdir '{}' \; 
  • Hey. That's awesome! Thanks. That did the trick almost entirely. I'm just having a minor issue, which is I'm getting foo1__qux1_bar5.txt, two underscores in the first folder level that is. Tried messing with your bash line but nothing seemed to work. Maybe I need to add something? – nunos Jan 8 '15 at 11:43
  • @nunos, I can't reproduce that behaviour – glenn jackman Jan 8 '15 at 11:52
  • Really? Weird. I just updated my answer with some more info. Can you please take a look? Thanks. – nunos Jan 8 '15 at 12:09
  • 1
    huh. OK, change "${file//\//_}" to "${file//+(/)/_, and if this is a script, shopt -s extglob – glenn jackman Jan 8 '15 at 12:23
  • I had the problem with the two underscores as well. Unfortunately, changing the command like you suggested did not work for me. I did find this answer on AskDifferent however: apple.stackexchange.com/a/254224/122152 According to the answer, macOS might be using BSD find. A workaround can be to use findutils. It can be installed using homebrew with brew install findutils and then using gfind instead of find. This worked for me. – intagli Feb 6 '18 at 22:15
5

Using perl's rename :

find . -depth -type f -exec rename 's@(?<!\.)/@_@g' -- {} \;

OUTPUT

$ find -type f
./foo2_bar4 whit a space.txt
./foo3_qux1_bar6.txt
./foo2_bar3.txt
./foo3_qux1_bar5.txt
./foo1_bar2.txt
./foo1_bar1.txt

NOTE

  • I use a negative look behind (?<!\.) to don't touch the first ./
  • I keep the empty dirs, feel free to :

    find . -depth -type d -exec rm {} \;

warning There are other tools with the same name which may or may not be able to do this, so be careful.

If you run the following command (GNU)

$ file "$(readlink -f "$(type -p rename)")"

and you have a result like

.../rename: Perl script, ASCII text executable

and not containing:

ELF

then this seems to be the right tool =)

If not, to make it the default (usually already the case) on Debian and derivative like Ubuntu :

$ sudo update-alternatives --set rename /path/to/rename

(replace /path/to/rename to the path of your perl's rename command.


If you don't have this command, search your package manager to install it or do it manually


Last but not least, this tool was originally written by Larry Wall, the Perl's dad.

  • ... -type d -exec rmdir ... ? – JJoao Jan 8 '15 at 11:22
  • I don't seem to have rename available.. How exactly can I get it and is this normally available in a UNIX system? Thanks. – nunos Jan 8 '15 at 12:04
  • @nunos It's a Perl script. It's available under the name perl-rename in Arch Linux, and prename in Debian-based systems. – muru Jan 8 '15 at 16:04
  • Added rename explanations – Gilles Quenot Jan 8 '15 at 17:01
1

with pax...

pax -Xrwls '|/|_|g' */ "$PWD"

That will create a hardlink in the current directory to all files in its child directories with a _ substituted for /. You can then inspect the results and remove all child directories with...

rm -rf */

...once you have verified the results are to your liking.

0

You can try with below command.

$ find -type f -exec bash -c 'mv $0 $(echo $0|sed "s/\//_/2")' {} \;

Above command will move the files and the directory will remain which can be removed later.

$ ls
foo1_bar1.txt  foo1_bar2.txt  foo2_bar3.txt  foo2_bar4.txt  foo3_bar5.txt  foo3_bar6.txt

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.