5

I have a directory structure like this:

Project/
  |
  +--Part1/
  |    |
  |    +--audio.mp3
  |
  +--Part2/
  |    |
  |    +--audio.mp3
  |
  +--Part3/
  |    |
  |    +--audio.mp3
...

I want to end up with files called Part1.mp3, Part2.mp3, etc.

Each folder only contains a single file so there is no risk of clobbering files or dealing with multiple files with the same name.

I feel like I could do this with some sort of find/xargs command coupled with cut and mv but I can't figure out how to actually form the command.

  • 1
    If you're willing to use zsh, zmv '(*)/audio.mp3' '$1.mp3' – Kevin Dec 31 '15 at 18:02
11

These examples work in any POSIX shell and require no external programs.

This stores the Part*.mp3 files at the same level as the Project directory:

(cd Project && for i in Part*/audio.mp3; do echo mv "$i" ../"${i%/*}".mp3; done)

This keeps the Part*.mp3 files in the Project directory:

for i in Project/Part*/audio.mp3; do echo mv "$i" ./"${i%/*}".mp3; done

These solutions use the shell's pattern matching parameter expansion to produce the new filename.

 ${parameter%word}     Remove Smallest Suffix Pattern.  The word is expanded
                       to produce a pattern.  The parameter expansion then
                       results in parameter, with the smallest portion of
                       the suffix matched by the pattern deleted.
  • oh yeah. i guess that proviso was dumb. sorry. it got my vote, though. i think mv -i would at least prompt if the target already exists, too, maybe. – mikeserv Dec 31 '15 at 2:33
  • 1
    @mikeserv Thanks for the fixes. I didn't want to get into mv -i etc because the question said "no risk of clobbering files" etc, although I often like to use mv -i. – RobertL Dec 31 '15 at 3:16
  • Why the .. in the first version? – Jonathan Leffler Dec 31 '15 at 14:51
  • @JonathanLeffler, because the shell has temporarily changed directory to the Project directory. The reason for the chdir is to simplify the new name generation. If there was no cd Project, then another step would be required inside the loop to remove the Project/ prefix. The change directory is "temporary" because the entire command is enclosed in parenthesis (), effectively creating a sub-shell. After the command completes, the current shell's directory is the same as before the parenthesized command. – RobertL Dec 31 '15 at 22:08
  • Doesn't the .. in the mv move the files to the level above the Project directory — at the same level as the name Project, rather than the directory holding the various Part directories? Try this script in a directory where you do not have a valuable sub-directory junk. It places the '.mp3' files one level too far up the directory hierarchy when I run it: rm -fr junk/Project; mkdir -p junk/Project; (cd junk/Project; mkdir Part{1,2,3}; for d in Part?; do echo > $d/audio.mp3; done); du -a junk; (cd junk/Project && for i in Part*/audio.mp3; do mv "$i" ../"${i%/*}".mp3; done); du -a junk; – Jonathan Leffler Dec 31 '15 at 22:24
7

If you have the perl rename (sometimes called prename) you can do this:

( cd Project && rename 's!(.+)/(.+)(\.mp3)!$1.$3!' */audio.mp3 )

This takes each filename matching the shell glob */audio.mp3 and splits it into the directory, filename, and extension components. It then discards the filename part and renames the file.

Use rename -n ... to see what would happen, or use -v instead of -n to watch it happening as it runs.

3
pax -rwls'|.*/\(.*\)/audio\(\.mp3\)$|\1\2|p' \
    -s'|.*||' /path/to/Project .

That will use the POSIX pax command-line archiver utility to make hardlinks in the current directory named like parent_dirs_base.mp3 for every audio.mp3 found rooted in the tree at /path/to/Project. While doing so it will print any filename modifications it makes to stderr.

Use -k to prevent it possibly overwriting existing files with identical names as its link targets, or, and better yet, first change to an empty directory before running it.

In my opinion, there is a significant advantage to hardlinking first rather than directly mving the files you seek to work with: and that is that you get the chance to verify that the results you expect are the same as those you get before removing originals. When you have checked you can:

find /path/to/Project -name audio.mp3 -exec rm {} +

...to clean up and cut the file links down by one.

  • I do wish I could get my head around pax. I keep forgetting to use it, though. Happy New Year mikeserv. – roaima Dec 31 '15 at 18:01
  • 1
    @roaima - happy new years to you! pax is pretty easy - s for substitute, r for read (extract), w for write (archive), l for link, and p for preserve permissions. to copy (or make hardlinks) you just r and w at the same time. you do pax -options [source[s]*] [destination?]. it does tar files - just more simply. it can also read names on stdin - and it always writes to stdout by default. when you do substitutions if the result is a null string that file's left out, if the s/// is otherwise successful the rest of the -s arguments are ignored. thats probably a lot of it – mikeserv Dec 31 '15 at 19:47
-1

You can try something like this:

These are your files:

$ cd Project
$ find . -type f
./Part2/audio.mp3
./Part3/audio.mp3
./Part1/audio.mp3

Using dirname will return the name of their directory (assuming you only have one level of sub-directories). Thus:

$ find . -type f \
   | while read i ; do \
       d=$(dirname $i); echo renaming "$i" to "$d.mp3" ; \
     done
renaming ./Part2/audio.mp3 to ./Part2.mp3
renaming ./Part3/audio.mp3 to ./Part3.mp3
renaming ./Part1/audio.mp3 to ./Part1.mp3

And this will rename them:

$ find . -type f | while read i ; do d=$(dirname $i); mv "$i" "$d.mp3" ; done
-1

Bash with perl style

for file in `find . -type f -name "*.mp3"`;
    do
        dir=$(dirname $file);
        bdir=$(basename $dir);
        bname=$(basename $file);
        new=$bdir.$bname;
        newname=${new//\.\./};
        newname_with_path=$dir/$newname
        if [ "$file" != "$newname_with_path" ]
        then
            echo mv -u $file  $newname_with_path ;
        fi
    done


    # replace 2 dots with nothing
    # bash 4.1.2 centos 6.4 no problem with or without bashslash
    newname=${new//../};
  • in case bdir is a dot and I have a dot to concat bdir and bname which make 2 dots, not pretty, thanks for looking – Gang Dec 31 '15 at 3:16
  • @RobertL, I edited and add more info to it, it is the bash version? – Gang Dec 31 '15 at 3:32
  • Sorry! I made a mistake. I didn't run it with bash. It works. Thanks. – RobertL Dec 31 '15 at 4:41
  • I deleted my incorrect statements because it could cause confusion for other readers. Please delete your comments if you think appropriate. Thanks for the explanation. – RobertL Dec 31 '15 at 22:11

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.