3

I have several folders with some files that I would like to rename from

Foo'Bar - Title

to

Title

I'm using OS X 10.7. I've looked at other solutions, but none that address recursion very well.

Any suggestions?

George K.
  • 2,867
  • 4
  • 19
  • 28

4 Answers4

10

There are two parts to your problem: Finding files to operate on recursively, and renaming them.

For the first, if everything is exactly one level below the current directory, you can just list the contents of every directory in the current directory (as in Mattias Wadman's answer above), but more generally (and possibly more easy to understand, to boot), you can just use the find command.

For the second, you can use sed and work out how to get the quoting and piping right (which you should definitely eventually learn), but it's much simpler to use the rename command. Unfortunately, this one isn't built in on Mac, but you can install it with, e.g., Homebrew, or just download the perl script and sudo install -m755 rename /usr/local/bin/rename.

So, you can do this:

find . -exec rename 's|[^/]* - ||' {} +

If you want to do a "dry run" to make sure it's right, add the "-n" flag to rename:

find . -exec rename -n 's|[^/]* - ||' {} +

To understand how it works, you really should read the tutorial for find, and the manpage for rename, but breaking it down:

  • find . means 'find all files recursively under the current directory'.
  • You can add additional tests to filter things (e.g., -type f if you want to skip everything but regular files, or `-name '*Title' if you want to only change files that end in 'Title'), but that isn't necessary for your use.
  • -exec+ means to batch up the found files, and pass as many of them as possible in place of any {} in the command that appears in the '…'.
  • rename 's|[^/]* - ||' {} means for each file in {}, apply the perl expression s|[^/]* - || to the filename, and, if the result is different, rename it to that result.
  • s|[^/]* - || means to match the regular expression '[^/]* -' and replace the match with '' (the empty string).
  • [^/]* - means to match any string of non-slash characters that ends with ' - '. So, in './A/FooBar - Title', it'll match the 'FooBar -'.

I should mention that, when I have something complicated to do like this, if after a few minutes and a couple attempts to get it right with find/sed/awk/rename/etc., I still haven't got it, I often just code it up imperatively with Python and os.walk. If you know Python, that might be easier for you to understand (although more verbose and less simple), and easier for you to modify to other use cases, so if you're interested, ask for that.

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • Yes, as I said in the other comment, there are at least three different implementations of rename, and at least one of them (embarrassingly, the one I actually linked to, and recommended…) will do the wrong thing as I originally wrote it. So I modified the regex to only match everything after the slash. – abarnert Jul 12 '12 at 21:30
  • Nice. I should probably use `find -exec` more often but i tend to use the `while read` pattern quite often as the seems easier to do multiple commands etc. – Mattias Wadman Jul 12 '12 at 21:33
  • You can write a function/script that chains the commands together, then find -exec that function. But as I said in the answer, if things get too complicated, I reach for a real language. – abarnert Jul 12 '12 at 21:35
  • Yeah exactly. I´ll upvote your answer as it is the most correct for the question... my answer became more of a shell tutorial :) time for sleep here! – Mattias Wadman Jul 12 '12 at 21:37
  • You can also filter the set of files that will be replaced by name. For example I needed to recursively rename all *.java files to *.txt so I did it like so: `find . -name "*.java" -exec rename 's|\.java|\.txt|' {} +` – Noam Ben Ari May 15 '14 at 12:25
  • How would you find string ignoring case, or multiple strings (e.g. find all foo, FOO, Foo, etc), my regexp would be /(foo|bar)/i – Nico Jan 19 '19 at 14:36
  • Ah here, I found it (just add another | and i): find . -exec rename -v 's|foo|Foo|i' {} + – Nico Jan 19 '19 at 16:22
3

Try this:

ls -1 * | while read f ; do mv "$f" "`echo $f | sed 's/^.* - //'`" ; done

I recommend you to add a echo before mv before running it to make sure the commands look ok. And as abarnert noted in the comments this command will only work for one directory at a time.

Detailed explanation of the various commands:

ls -1 * will output a line for each file (and directory) in the current directory (except .-files). So this will be expanded in to ls -1 file1 file2 ..., -1 to ls tells it to list the filename only and one file per line.

The output is then piped into while read f ; ... ; done which will loop while read f returns zero, which it does until it reaches end of file. read f reads one line at a time from standard input (which in this case is the output from ls -1 ...) and store it in the the variable specified, in this case f.

In the while loop we run a mv command with two arguments, first "$f" as the source file (note the quotes to handle filenames with spaces etc) and second the destination filename which uses sed and ` (backticks) to do what is called command substitution that will call the command inside the backticks and be replaced it with the output from standard output.

echo $f | sed 's/^.* - //' pipes the current file $f into sed that will match a regular expression and do substitution (the s in s/) and output the result on standard output. The regular expression is ^.* - which will match from the start of the string ^ (called anchoring) and then any characters .* followed by - and replace it with the empty string (the string between //).

Mattias Wadman
  • 11,172
  • 2
  • 42
  • 57
  • Thank you! Would you mind explaining what that does? I'd wanna learn this – George K. Jul 12 '12 at 19:32
  • This doesn't do what the user wants. For example: 'mkdir A B ; touch "A/FooBar - Title" "B/FooBar - Title"; ls -1 * | while read f ; do mv "$f" "`echo $f | sed 's/^.* - //'`" ; done will just output 6 errors. – abarnert Jul 12 '12 at 20:55
  • Sure! i've updated the answer with some detailed explanations. I think there is a tool called `rename` or something similar that could do this much easier. – Mattias Wadman Jul 12 '12 at 21:04
  • But i guess you should use `find` instead of `ls` if you really want to do this recursively and only include files etc. – Mattias Wadman Jul 12 '12 at 21:09
  • No, `ls -1 */*` matches nothing at all in the example. It finds all files exactly two levels below the current directory, but all of the files are one level below. – abarnert Jul 12 '12 at 21:10
  • Look at what `ls -1` returns. It's not "A/FooBar - Title" "B/FooBar - Title", it's "" "A:" "FooBar - Title" "" "B:" "FooBar - Title". You could `ls */*Title`, which would work here. But then, once you match the right thing, your mv will move all of the files to Title (not A/Title, etc.), which I doubt you want. – abarnert Jul 12 '12 at 21:13
  • 1
    Ah your right, the sed command will remove the directory which is bad. Will `rename` have the same problem when using the `s/^.* - //` regex? – Mattias Wadman Jul 12 '12 at 21:20
  • Actually, it depends on which version of rename you get, which is pretty bad… I'll fix my version. – abarnert Jul 12 '12 at 21:27
2

I know you asked for batch rename, but I suggest you to use Automator. It works perfectly, and if you create it as a service you will have the option in your contextual menu :)

Tim Autin
  • 6,043
  • 5
  • 46
  • 76
1

After some trial and error, I came across this solution that worked for me to solve the same problem.

find <dir> -name *.<oldExt> -exec rename -S .<oldExt> .<newExt> {} \;

Basically, I leverage the find and rename utilities. The trick here is figuring out where to place the '{}' (which represents the files that need to be processed by rename) of rename.

P.S. rename is not a built-in linux utility. I work with OS X and used homebrew to install rename.

eddie
  • 21
  • 5