5

I have a directory structure like this:

./a/1.png
./a/2.png
./a/3.png
./b/1.png
./b/2.png
./b/3.png
./c/1.png
...

And I want to take all the files in the subdirectories and move them to a new directory so their names are something like

../dest/a_1.png
../dest/a_2.png
../dest/a_3.png
../dest/b_1.png
../dest/b_2.png
../dest/b_3.png
../dest/c_1.png
...

The closest I've been able to find without writing a script to do it file by file is to use find with the --backup=numbered option which would condense my files to a single directory but would end up losing the directory context from the filename.

Is there a succinct way to accomplish this?

2
  • 1
    Why not make a script? – var firstName Aug 21 '17 at 16:42
  • I did end up writing a (super inefficient) script (in nodejs.. it's what I know best) but was hoping to learn something new :) – Brad Dwyer Aug 21 '17 at 17:23
11

With Perl's standalone rename command:

rename -n 's|/|_|; s|^|dest/|' */*.png

Output:

a/1.png renamed as dest/a_1.png
a/2.png renamed as dest/a_2.png
a/3.png renamed as dest/a_3.png
b/1.png renamed as dest/b_1.png
b/2.png renamed as dest/b_2.png
b/3.png renamed as dest/b_3.png
c/1.png renamed as dest/c_1.png

If everything looks fine, remove option -n.

3
  • 1
    Heh, very clever, just replacing / with _ does the job in this particular case, of course. – slhck Aug 21 '17 at 19:50
  • Nice, I didn't know about Perl rename -- looks useful! – Brad Dwyer Aug 22 '17 at 16:37
  • The other approach of Jochen Lutz with mmv is also possible: rename -n 's|(.*)/(.*)|dest/$1_$2|' */*.png – Cyrus Aug 23 '17 at 4:53
6

With Bash 4 and recursive globbing (shopt -s globstar):

for f in **/*.png; do
  dn=$(basename "$(dirname "$f")")
  bn=$(basename "$f")
  mv -- "$f" "../dest/${dn}_${bn}"
done

Note that dirname /foo/bar/baz returns /foo/bar and not just bar, which is why you have to call basename on the result to just get bar in case you are working from another parent folder.

3

As an alternative approach, you don't need external programs like rename, basename, etc - it can all be handled within bash parameter expansion:-

find SourceDir ... | while read -r f; do mv "$f" "TargetDir/${f//\//_}"; done

The expansion is a bit difficult to follow, so here's what happens:-

  • The files to be moved are found with find.
  • Each file name is read in turn into $f.
  • The read -r parameter and the double quotes around the expansions handle strange names, including spaces in the file names.
  • The mv command moves names like a/b/c to TargetDir/a_b_c.
  • The target expansion replaces every / by _, but it looks daunting because / is part of the substitution syntax.
  • The general form is ${param/old/new}, which replaces the first instance of old by new in the expansion.
  • The form needed here is ${param//old/new}, which replaces every instance of old by new in the expansion.
  • In order for / to be part of the old string, it must be escaped as \/, hence the rather obscure ${f//\//_}: the first / introduces the substitution syntax, the second / specifies replace every, the third (escaped) / is the old string, and the final / introduces the new string (_).

I don't often see this form of expansion in scripts, but it is worth knowing about, as it can be very useful at times.

There are some file names which will break this (embedded new-line characters, and leading or trailing spaces, though there are two ways round the latter:-

  • Use read -r instead of read -r f and use REPLY instead of f.
  • Use while f="$(line)" instead of read -r f.

The latter is neater, but uses the external program line, which may not be available on all systems, though it can be coded as a function:

line() { read -r; r=$?; echo "$REPLY"; return $r; }
2
  • Re "it can all be handled within bash parameter expansion", that rather assumes that you're using bash, no? Whereas Perl &c are shell-agnostic, AFAIK. – jamesqf Aug 22 '17 at 5:52
  • 1
    @jamesqf - The questioner included bash as a tag, hence the assumption. – AFH Aug 22 '17 at 9:30
2

As an alternative to rename, there is mmv, which uses standard shell patterns.

From the man page:

Mmv moves (or copies, appends, or links, as specified) each source file matching a from pattern to the target name specified by the to pattern. This multiple action is performed safely, i.e. without any unexpected deletion of files due to collisions of target names with existing filenames or with other target names. Furthermore, before doing anything, mmv attempts to detect any errors that would result from the entire set of actions specified and gives the user the choice of either proceeding by avoiding the offending parts or aborting.

For your use case:

mmv -n '*/*' 'dest/#1_#2'
a/1.jpg -> dest/a_1.jpg
a/2.jpg -> dest/a_2.jpg
a/3.jpg -> dest/a_3.jpg
b/1.jpg -> dest/b_1.jpg
b/2.jpg -> dest/b_2.jpg
b/3.jpg -> dest/b_3.jpg
c/1.jpg -> dest/c_1.jpg
c/2.jpg -> dest/c_2.jpg
c/3.jpg -> dest/c_3.jpg

Like rename, '-n' is no-execute. Remove it for actual renaming.

Besides renaming, mmv also supports copying, linking, or appending the files.

This site is temporarily in read only mode and not accepting new answers.

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