0

in OSX Terminal, I try to flatten a folder hierarchy, but as the files inside are named identical, I want to extend the filenames with its former folder names:

I'd like to get from here:

/dirA1/dirB1/file1.ext
/dirA1/dirB2/file1.ext
...
/dirA2/dirB1/file1.ext
...

to

/file1-dirA1-dirB1.ext
/file1-dirA1-dirB2.ext
...
/file1-dirA2-dirB1.ext
...

I've tried to combine renaming (Batch renaming files in OSX terminal) with flattening (https://unix.stackexchange.com/questions/52814) but no luck yet ...

Would I start with "find"? But how can I pass the directory names to "mv"?

Thank you very much in advance!

Community
  • 1
  • 1
Frank
  • 1
  • 2

2 Answers2

0

A safe, but not very fast way would be to use find with -exec, passing each file to a shell command where you perform the replacements, like this:

(cd /path; find . -type f -exec sh -c 'newname=${1:2}; filename=${newname##*/}; dirname=${newname%/*}; newname=$filename-${dirname//\//-}; echo mv "$1" "$newname"' -- {} \;)

The reason for the cd /path instead of find /path is to so that we can work with relative paths from that base directory, which makes it easier to create the flattened file names.

A bit more explanation of the elements:

  • -exec sh -c '...' -- {} executes sh, running the commands specified in -c '...'. The {} is the path found by find. The -- ... syntax is to pass parameters to the script as positional arguments, accessible via $1, $2, and so on.
  • newname=${1:2} takes $1, the first positional argument, and extracts a sub-string from it, skipping the first two characters. I do this because the output of find . will have paths starting with ./, and we want to remove that before replacing the / with -.
  • filename=${newname##*/} extracts the filename part of the path, by removing everything from the beginning until the last /
  • dirname=${newname%/*} extracts the directory name part of the path, by removing the last / and everything after it
  • ${dirname//\//-} replaces all occurrences of / with -. If you want to remove the / instead of replacing, then you can write ${dirname//\//}

This technique is safe, because it will work with any special characters in filenames. It's slow because it runs sh for each file.

After this step the empty directories of the original files will remain, and you probably want to remove them:

find /path -type d -empty -delete
janos
  • 120,954
  • 29
  • 226
  • 236
  • Thanks! Will try asap. – Could you enlighten me (or point me to) what the newname=${1:2} and the newname//\/- each exactly do? – Frank Feb 11 '17 at 12:47
  • I added more explanation, and fixed an important typo. The replacement should be `newname=${newname//\//-}`, and not `newname=${newname//\/-}` as it was before my edit. – janos Feb 11 '17 at 13:01
  • Thanks, Janos! Actually, I'd like the directory names appended /after/ the original filename. How to I tell the mv to append the arguments instead of prepending them? (Also, how could I completely remove the / character instead of replacing it with a -? I can't replace with a "no character", right?) – Frank Feb 11 '17 at 18:43
0

You can safely handle this without executing sh for every file by feeding find … -print0 output to a while read loop, like:

while IFS= read -r -d '' old; do            # read filenames into $old
                                            # -d ''  : read until \0
                                            # -r     : no backslash escapes
                                            # IFS=   : no trimming whitespace
    if [[ $old =~ (.*)/(.*)\.(.*) ]]; then  # regex with capture groups:
       #           1    2     3
       set -- "${BASH_REMATCH[@]}"
       mv "$old" "${2}-${1//\/}.${3}"
    fi
done < <(find . -type f -print 0)

The purpose of the set -- "${BASH_REMATCH[@]}" above is just to move the members of the array that contains regex match groups into the positional parameters ($1, $2, etc) for legibility.

Grisha Levit
  • 8,194
  • 2
  • 38
  • 53