6

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? Commented Aug 21, 2017 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
    Commented Aug 21, 2017 at 17:23

4 Answers 4

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
    Commented Aug 21, 2017 at 19:50
  • Nice, I didn't know about Perl rename -- looks useful!
    – Brad Dwyer
    Commented Aug 22, 2017 at 16:37
  • The other approach of Jochen Lutz with mmv is also possible: rename -n 's|(.*)/(.*)|dest/$1_$2|' */*.png
    – Cyrus
    Commented Aug 23, 2017 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
    Commented Aug 22, 2017 at 5:52
  • 1
    @jamesqf - The questioner included bash as a tag, hence the assumption.
    – AFH
    Commented Aug 22, 2017 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.

You must log in to answer this question.

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