Calling *NIX shell wizards.
December 11, 2005 4:04 PM   Subscribe

Calling *NIX shell wizards.

Given a directory with files whose names obey this format:

01 sometitle.mp3
02 anothertitle.mp3
...
13 someothertitle.mp3


etc.

How do I clip the track number and trailing space from each filename?

I got as far as
find . -name "*.mp3" -exec mv {} $(echo {} | sed -e 's/^..//') \;

but that doesn't work: $(echo {} | sed -e 's/^..//') expands to a blank, and so mv complains of a missing argument.

I figure I'm not really understanding how sed redirects its output. I'm trying to figure out what exactly I did wrong. I'm trying to teach myself something here, so no perl solutions or anything like that as they'd get the job done but won't teach me much at this stage.
posted by ori to Computers & Internet (33 answers total) 1 user marked this as a favorite
 
not entirely sure, but I think you need to escape a few more things. For example,

mv {} \$\(echo {} \| sed -e 's/^..//'\) \;

might work better (I don't do a lot of shell programming through).
posted by sbutler at 4:14 PM on December 11, 2005


foreach file (*.mp3) {
mv $file `echo $file|sed s/\d+ //`
}

.. adapt for your favorite shell. that's pseudo-tcsh and the regex is likely not quite right off the top of my head.
posted by kcm at 4:15 PM on December 11, 2005


Although its not what you want, the following python script works (save as "fixnames.py" and then run "python fixnames.py").

-----------------------------
#!/usr/bin/python

import os
import string
import sys

if len(sys.argv) > 1:
    musicDir = sys.argv[1]
else:
    musicDir = '.'

musicDir = os.path.abspath(musicDir)
if not os.path.exists(musicDir):
    sys.exit('Invalid directory')

filenames = [name for name in os.listdir(musicDir) if name.endswith('.mp3')]
for name in filenames:
    newName = name.lstrip(string.digits + ' ')
    os.rename(os.path.join(musicDir, name), os.path.join(musicDir, newName))
------------------------
posted by gsteff at 4:34 PM on December 11, 2005


I realize this is brutally ugly, but...

ls | cut -b 3-

Might work, if you've got a standard length in the front, of course.
posted by Orb2069 at 4:57 PM on December 11, 2005


for foo in *.mp3; do
mv ${foo} `echo ${foo} | sed 's/^[0-9][0-9] //'`
done
posted by I Love Tacos at 4:58 PM on December 11, 2005


This is ugly, but at least it's not as ugly as python:
find . -type f -name '*mp3' | while read file
do
 mv "$file" "`echo $file | sed 's/[0-9][0-9] //;'`"
done
You've got to be careful of the spaces in the file names. Also note that I used backticks; ICGAFF if they're "deprecated" -- I have scripts older than folks who advocate this change.

This works under OS X bash, and probably other bashes too.
posted by scruss at 5:06 PM on December 11, 2005


Wow, the above answers may work, but maaan are they ugly.

How do I clip the track number and trailing space from each filename?


I don't quite get what you are saying... if your files are in the form of: Foobar - Quux 01.mp3

You want...

rename 's/\d+ \.mp3/\.mp3/' *.mp3

The rename command is a utility that ships with perl.
posted by phrontist at 5:10 PM on December 11, 2005


Scruss: Your answer will remove any two numbers followed by spaces anywhere in the song title, no?

not as ugly as python

Is anything? Oh, that's right, Ruby.
posted by phrontist at 5:13 PM on December 11, 2005


Why still use hairy shell syntax when there are so many nice scripting languages available nowadays?

The ruby oneliner:

Dir.open('.'){|d| d.entries.each{|e| File.rename(e, e.split(' ')[1..-1].join(' ')) if e =~ /^[0-9]+ .*\.mp3$/}}
posted by koenie at 5:15 PM on December 11, 2005


Ugh, I just realized your numbers are in front of the file, my bad, that should be:

rename 's/^\d+ //' *.mp3

posted by phrontist at 5:15 PM on December 11, 2005


For the record, the python script above can be done as a two liner; python coders just don't like to do that.
posted by gsteff at 5:23 PM on December 11, 2005


Well... if we want to talk about one-liners, here's one for perl:

use File::Find;
find( sub { -e $1 || rename $_, $1 if /^\d+\s+(.*\.mp3)$/i }, '.');

or if you're going to be pedantic...

require File::Find && import File::Find || find( sub { -e $1 || rename $_, $1 if /^\d+\s+(.*\.mp3)$/i }, '.');

It even has the bonus of only renaming a file if it won't overwrite a current one!
posted by sbutler at 5:40 PM on December 11, 2005


For the record, the python script above can be done as a two liner; python coders just don't like to do that.

Yea, your example is great for a "small utility" level of coding, but I personally in that case would've just cd'd to the directory in question, fired up the Python shell or ipython, and just ran something like:

    import os
    [os.rename(x,x[3:]) for x in os.listdir('.')]

which is quite succinct, I think :)

It makes a buttload of assumptions, which your script of course checks, but assuming you know that there are, for example, less than 100 files, they're all mp3s, and all the single-digit tracks have a leading zero as the example shows...it's a whole lot less work.

ori: this is why these days, it's probably more worth your while to learn Python (or Ruby or Perl) and skim the 'OS' or 'system' libraries, than it is to try and learn your shell's scripting language. You can usually do things much easier, and (well, in Python anyways ;)) with a much more sane syntax, than bash/tcsh/etc.
posted by cyrusdogstar at 5:52 PM on December 11, 2005


sbutler: My answer is technically perl...
posted by phrontist at 5:52 PM on December 11, 2005


Instead of sed, you could use awk... I mean, sed is usually for doing replacements in strings, while awk is for tokenizing or transforming them:

ls | awk '{print $2}'

would you get you the corrected file names, assuming there are no spaces in the title of the track. If there are, you could do it like:

ls | awk '{print $1$2$3$4$5$6}'
posted by gus at 5:57 PM on December 11, 2005


I like I love tacos' and scruss' solutions best, personally. (Actually orb2069 and phrontis both have great answers too.) However, if you really want your one-liner bash script to work more-or-less as you describe above, here's the best I can come up with:

find -name '*.mp3' -exec sh -c "mv \"{}\" \"\`echo {} | sed -e 's/[0-9]\+ //'\`\"" \;

It's magnificently ugly. The problem is the shell expansion (the sed in backticks) needs to happen at "runtime," while find is working. If you just type that on the command line it'll run before it passes the parameters off to find, which isn't what you want - and if you escape it then find will pass those literals to its command. (Incidentally, I'm not familiar with the convention you're using to execute a command with "$(" - bash may well support it, but I'm using backticks in my example.) The only way I know of to manage that is to surround the whole thing in sh -c. Which means you have to escape almost everything.
posted by mragreeable at 5:59 PM on December 11, 2005


Use the answers that do everything with a single invocation of perl or python. All these answers that spawn an invocation of mv and sed for each file are going to be hideously slow if you have a large number of files to rename.
posted by Rhomboid at 6:12 PM on December 11, 2005


phrontist, it'll replace the first incidence of numeral-numeral-space in the file name, which is probably what you want. Remember that 'find .' will precede everything with './', so using a regex with '^' isn't going to work.

koenie and cyrusdogstar, you are taking the piss about nice clean syntax in those scripting languages, no?

Tip o' the hat to mragreeable for doing what you wanted to do, and actually getting it to work. Now that's what I call clean syntax!
posted by scruss at 6:19 PM on December 11, 2005


Well, I guess my example and my note about syntax were a bit at odds, I was doing two things at once :) A nice, clean syntactical version of my example would be:

    import os
    for filename in os.listdir('.'):
        new_filename = filename[3:]
        os.rename(filename,new_filename)
posted by cyrusdogstar at 6:25 PM on December 11, 2005


Rhomboid, one never writes shell for speed, merely expediency. It just took my machine 38s to rename 1000 files with my while and sed method. Slow? Maybe, but I bet it'd take more than 38s to think of a more elegant solution.
posted by scruss at 6:39 PM on December 11, 2005


Nothing more entertaining or insightful than a scripting language flame war. On second thought, there's one thing....

I hope you all edited your scripts with the best editor in the world, vi.

(gets popcorn)
posted by I Love Tacos at 7:16 PM on December 11, 2005


I Love Tacos: Actually, I waved magnets around my hard-drive, I'm a sucker for intuitive user interfaces.
posted by phrontist at 7:53 PM on December 11, 2005


mragreeable understands the problem. Your sed script is operating on the characters "{}" not your file name. find is substituting the file name too late for your method. If you replace "mv" with "echo", you get to see what "mv" sees.
posted by gearspring at 9:20 PM on December 11, 2005


rename 's/^\d+ //' *.mp3

This won't work with my rename, which is regexp-ignorant.

This would work:

for x in *.mp3; do mv "$x" `echo "$x" | cut -d ' ' -f 2`; done

This assumes that there really is only one space in the original filenames, which accords with the scheme you give. Why go to the trouble of sed or awk for such a simple change?
posted by kenko at 9:32 PM on December 11, 2005


Well, I can't help the original poster - the shell is no place for this kind of thing - but perl -lane is your friend:

ls | perl -lane 'rename $_, $F[1]'

-l gets rid of the trailing space
-a splits on whitespace
-n does it once for each line of stdin
-e runs the script, which renames each line of stdin to what -a puts in the second entry of the @F array
posted by nicwolff at 10:03 PM on December 11, 2005


I wrote a Python script called renamer which should do the trick nicely. (Except for the whole trying to teach yourself sed thing, RTFQ, I know, I know.) Works like this:

$ renamer --perl-regexp="^\d\d " "" *.mp3
'01 xyzzy.mp3' -> 'xyzzy.mp3'
'02 asdfasdxyzzy.mp3' -> 'asdfasdxyzzy.mp3'
'03 foo.mp3' -> 'foo.mp3'


Easy, no? I actually originally wrote this mainly to rename MP3s of all things and because I was sick of screwing around with find+sed/perl+xargs to do the same sort of thing over and over.
posted by grouse at 2:08 AM on December 12, 2005


I think you're looking at the problem from the wrong angle. What you normally do is tag the MP3s with ID3v2 tags, and afterwards you can automatically rename all the files with whatever naming scheme you want based on the tag information.

There are many ways to automate the tagging process. On Windows I use Foobar2000 with FreeDB, or MusicBrainz tagger, but I'm sure that there are similar tools for Linux. On Windows, Total Commander has an awesome rename tool.
posted by Sharcho at 3:54 AM on December 12, 2005


I find it rather disturbing that this many answers have gone by with nobody (far as I saw) explaining what's wrong with the original attempt:

find . -exec mv {} $(echo {} | sed -e 's/^..//') \;

What happens is that echo is giving the output "{}", and sed is replacing that with an empty string. The shell does its thing before find does.

Also, find will give you output like "./01 blah.mp3", so your regex wouldn't work anyway.

The way I'd do it (in bash) is:
for i in `find . -name \*.mp3` ; do mv $i ${i/\/[0-9][0-9] //} ; done
Don't forget to set IFS to newline.

posted by sfenders at 7:32 AM on December 12, 2005


Well, I can't help the original poster - the shell is no place for this kind of thing

1. Your solution was shell-based.
2. This is a ridiculously simple task, for which the shell is absolutely suited.

I actually originally wrote this mainly to rename MP3s of all things

Hey, I wrote a script for that too.
posted by kenko at 11:12 AM on December 12, 2005


1. Your solution was shell-based.

WTF? It uses the shell to ls and launch one perl process (as compared to your shell-only solution, which starts one shell and one copy of 'cut' for each file, gah).
posted by nicwolff at 11:56 AM on December 12, 2005


Rhomboid, one never writes shell for speed, merely expediency.
Speak for yourself. As a programmer there is something inherently appealing about finding the "correct" solution and just going with the first thing that works, however ugly it may be. If that doesn't appeal to you, then fine. But don't assume that just because you don't care that there's no reason to worry about these things.

In fact you have here a whole thread of many possible solutions. In such a case it's helpful to have a criteria to judge the resulting solutions on, such as "doesn't deal with filenames with spaces in them" or "is ridiculously inefficient".
posted by Rhomboid at 12:44 PM on December 12, 2005


hmm, I see mragreeable had the right answer. But it can be made slightly less ugly than that. Use ' in place of " so you don't have to escape everything. " can be nested inside of `, too:

find . -name '*.mp3' -exec sh -c 'mv {} "`echo {} | sed -r "s/[0-9]+ //"`" ' \;

Also:

for i in *.mp3 ; do mv "$i" "`echo $i | sed -r 's/[0-9]+ //'`" ; done

Or even:

ls *mp3 | grep '^[0-9]' | sed -r 's/([0-9]+ )(.*)/"\1\2" "\2"/' | xargs -n2 mv
posted by sfenders at 3:27 PM on December 12, 2005


bash can do what's required without any help from sed or perl or python:

for f in *; do mv "$f" "${f:3}"; done

posted by flabdablet at 9:03 AM on December 13, 2005


« Older hebrew   |   How to Clean a Cloudy Sink? Newer »
This thread is closed to new comments.