91

I have a set of files named like:

Friends - 6x03 - Tow Ross' Denial.srt
Friends - 6x20 - Tow Mac and C.H.E.E.S.E..srt
Friends - 6x05 - Tow Joey's Porshe.srt

and I want to rename them like the following

S06E03.srt
S06E20.srt
S06E05.srt

what should I do to make the job done in linux terminal? I have installed rename but U get errors using the following:

rename -n 's/(\w+) - (\d{1})x(\d{2})*$/S0$2E$3\.srt/' *.srt
Thor
  • 45,082
  • 11
  • 119
  • 130
orezvani
  • 3,595
  • 8
  • 43
  • 57

10 Answers10

105

You forgot a dot in front of the asterisk:

rename -n 's/(\w+) - (\d{1})x(\d{2}).*$/S0$2E$3\.srt/' *.srt

On OpenSUSE, RedHat, Gentoo you have to use Perl version of rename. This answer shows how to obtain it. On Arch, the package is called perl-rename.

Thor
  • 45,082
  • 11
  • 119
  • 130
  • 7
    OpenSUSE, RedHat, Gentoo doesn't support regex in `rename` – maresmar Dec 15 '16 at 18:32
  • 1
    @mmrmartin: The rename script used here is the one written by Larry Wall. It used be in the file `/usr/bin/rename`, but perhaps it has been renamed (no pun intended)? On Debian the script name is now `/usr/bin/file-rename`. – Thor Dec 15 '16 at 18:42
  • 5
    openSUSE uses rename from `util-linux` package, I didn't find any package providing `file-rename`, `prename` or `perl-rename` - only working solution was [install using cpan](http://stackoverflow.com/a/32862278/1392034) for me. – maresmar Dec 15 '16 at 19:26
  • @mmrmartin Same problem on RHEL 6, which also uses `rename` based on `util-linux`. See https://stackoverflow.com/a/48280659/1236128. – Jonathan Komar Jan 16 '18 at 11:57
46

find + perl + xargs + mv

xargs -n2 makes it possible to print two arguments per line. When combined with Perl's print $_ (to print the $STDIN first), it makes for a powerful renaming tool.

find . -type f | perl -pe 'print $_; s/input/output/' | xargs -d "\n" -n2 mv

Results of perl -pe 'print $_; s/OldName/NewName/' | xargs -n2 end up being:

OldName1.ext    NewName1.ext
OldName2.ext    NewName2.ext
OldName3.ext    NewName3.ext
OldName4.ext    NewName4.ext

I did not have Perl's rename readily available on my system.


How does it work?

  1. find . -type f outputs file paths (or file names...you control what gets processed by regex here!)
  2. -p prints file paths that were processed by regex, -e executes inline script
  3. print $_ prints the original file name first (independent of -p)
  4. -d "\n" cuts the input by newline, instead of default space character
  5. -n2 prints two elements per line
  6. mv gets the input of the previous line

My preferred approach, albeit more advanced.

Let's say I want to rename all ".txt" files to be ".md" files:

find . -type f -printf '%P\0' | perl -0 -l0 -pe 'print $_; s/(.*)\.txt/$1\.md/' | xargs -0 -n 2 mv

The magic here is that each process in the pipeline supports the null byte (0x00) that is used as a delimiter as opposed to spaces or newlines. The first aforementioned method uses newlines as separators. Note that I tried to easily support find . without using subprocesses. Be careful here (you might want to check your output of find before you run in through a regular expression match, or worse, a destructive command like mv).

How it works (abridged to include only changes from above)

  1. In find: -printf '%P\0' print only name of files without path followed by null byte. Adjust to your use case-whether matching filenames or entire paths.
  2. In perl and xargs: -0 stdin delimiter is the null byte (rather than space)
  3. In perl: -l0 stdout delimiter is the null byte (in octal 000)
Jonathan Komar
  • 2,678
  • 4
  • 32
  • 43
  • 5
    For me, this is the best answer - oneliner with tools available out of the box – Koikos Oct 24 '18 at 18:29
  • 2
    this erased all my files. luckily I made a backup – exebook Sep 13 '19 at 20:35
  • 9
    The last command should be changed to `xargs -d '\n' -n2 mv`, otherwise xargs will treat spaces in filenames as delimiters and either cause errors, or rename files nonsensically. The `-d '\n'` argument specifies that newlines should be treated as the delimiter. GNU xargs has the `-d` argument, but for those implementations that do not (i.e. FreeBSD which I was using), this would work across most environments: `find . -type f | perl -pe 'print $_; s/input/output/' | sed 's/ /\\ /g' xargs -n2 mv` by using `sed` to escape all spaces in the output that's piped to `xargs`. (Not elegant, perhaps.) – s.co.tt Nov 04 '19 at 20:32
  • 4
    @s.co.tt A perhaps better way to treat spaces as normal chars is to use a different dilimiter char. Xargs supports the 0-byte and so does find. I‘d do a `find -print0` followed by a `xargs -0`. – Jonathan Komar Jan 15 '20 at 07:07
  • 1
    Another improvement would be to pre-filter the results from the find through grep to minimize the no-op renames: `find . -type f | grep 'input' | perl -pe 'print $_; s/input/output/' | xargs -n2 mv` – beporter Jun 24 '20 at 18:07
  • "SomeFile.1400mb.mkv" ... To remove "1400mb" (e.g., file size): ```for f in `find -type f`; do mv -v "$f" "`echo $f | sed -r 's/[0-9]{1,}.*mb/ /'`"; done``` Note use of backticks: ` : https://unix.stackexchange.com/questions/27428/what-does-backquote-backtick-mean-in-commands – Victoria Stuart Dec 10 '20 at 19:50
  • @VictoriaStuart Seems like your comment should be an answer, but regardless, I would not recommend your approach, see https://stackoverflow.com/a/9612560 – Jonathan Komar Jul 08 '21 at 15:23
16

Use mmv (mass-move?)

It's simple but useful: The * wildcard matches any string (without /) and ? matches any character in the string to be matched. In the replace string, use #N to refer to the N-th wildcard match.

In your case:

mmv 'Friends - 6x?? - Tow *.srt' 'S06E#1#2.srt'

Here, #1#2 represent the two digits which are captured by ?? (match #1 and #2).
So the following replacement is being made:

The pattern string:     'Friends - 6x?? - Tow *           .srt'
matches this file:       Friends - 6x03 - Tow Ross' Denial.srt
                                     ↓↓
will be renamed to:             S06E03.srt

Personally, I use it to pad numbers such that numbered files appear in the desired order when sorted lexicographically (e.g., 1. appears before 10.): file_?.extfile_0#1.ext


mmv also offers matching by [ and ] and ;.

You can not only mass rename, but also mass move, copy, append and link files.

See the man page for more!

xoxox
  • 719
  • 1
  • 13
  • 24
12

Edit: found a better way to list the files without using IFS and ls while still being sh compliant.

I would do a shell script for that:

#!/bin/sh
for file in *.srt; do
  if [ -e "$file" ]; then
    newname=`echo "$file" | sed 's/^.*\([0-9]\+\)x\([0-9]\+\).*$/S0\1E\2.srt/'`
    mv "$file" "$newname"
  fi
done

Previous script:

#!/bin/sh
IFS='
'
for file in `ls -1 *.srt`; do
  newname=`echo "$file" | sed 's/^.*\([0-9]\+\)x\([0-9]\+\).*$/S0\1E\2.srt/'`
  mv "$file" "$newname"
done
Creak
  • 4,021
  • 3
  • 20
  • 25
  • What does `IFS='\n'` stand for in this example? I like it because it does not use anything special. – Sobvan Jun 16 '17 at 11:25
  • IFS: The Internal Field Separator that is used for word splitting after expansion and to split lines into words with the read builtin command. The default value is "" -- (from man bash). Changing it to `\n` allows to get one file per line. – Creak Jun 17 '17 at 16:36
  • You could extend the script to support recursive action with: ``for file in `find . -type f`; do`` (But then you need to update the sed to capture the path also) – Goran.it Oct 18 '18 at 10:37
10

Not every distro ships a rename utility that supports regexes as used in the examples above - RedHat, Gentoo and their derivatives amongst others.

Alternatives to try to use are perl-rename and mmv.

gerrit_hoekstra
  • 509
  • 6
  • 8
7

if your linux does not offer rename, you could also use the following:

find . -type f -name "Friends*" -execdir bash -c 'mv "$1" "${1/\w+\s*-\s*(\d)x(\d+).*$/S0\1E\2.srt}"' _ {} \;

i use this snippet quite often to perform substitutions with regex in my console.

i am not very good in shell-stuff, but as far as i understand this code, its explanation would be like: the search results of your find will be passed on to a bash-command (bash -c) where your search result will be inside of $1 as source file. the target that follows is the result of a substitution within a subshell, where the content of $1 (here: just 1 inside your parameter-substituion {1//find/replace}) will also be your search result. the {} passes it on to the content of -execdir

better explanations would be appreciated a lot :)

please note: i only copy-pasted your regex; please test it first with example files. depending on your system you might need to change \d and \w to character classes like [[:digit:]] or [[:alpha:]]. however, \1 should work for the groups.

meistermuh
  • 393
  • 3
  • 11
  • 1
    As the bash manual says: "-c string If the -c option is present, then commands are read from string. If there are arguments after the string, they are assigned to the positional parameters, starting with $0.", so you can even improve your command: `find . -type f -name "Friends*" -execdir bash -c 'mv "$0" "${0/\w+\s*-\s*(\d)x(\d+).*$/S0\1E\2.srt}"' {} \;` – Louis Caron Sep 22 '21 at 12:53
6

I think the simplest as well as universal way will be using for loop sed and mv. First, you can check your regex substitutions in a pipe:

ls *.srt | sed -E 's/.* ([0-9])x([0-9]{2}) .*(\.srt)/S\1E\2\3/g'

If it prints the correct substitution, just put it in a for loop with mv

for i in $(ls *.srt); do 
    mv $i $(echo $i | sed -E 's/.* ([0-9])x([0-9]{2}) .*(\.srt)/S\1E\2\3/g') 
    done
5

Use regex-rename

It's super easy to install (unlike the other tools):

pip3 install regex-rename

Do the renaming with:

regex-rename "(\d{1})x(\d{2})" "S0\1E\2.srt" --rename

Try "dry-run" mode (without --rename flag) in first place to check if it looks good before the actual renaming. It shows you what was matched to each of the groups so you can debug your regex until it's fine. It expects 2 arguments: matcher & replacement pattern, no bizarre s/.../.../ syntax. Also, I'm too lazy to do a full match, it just works with the season + episode pattern.

I made it myself as I saw there's no decent tool like this. I'd love to hear your feedback.

igrek51
  • 177
  • 1
  • 9
  • Seems very slick. Would love to be able to pass it files via find so there is more control over /not/ entering directories, etc – Chris Feb 07 '23 at 01:47
2

You can use rnm:

rnm -rs '/\w+\s*-\s*(\d)x(\d+).*$/S0\1E\2.srt/' *.srt

Explanation:

  1. -rs : replace string of the form /search_regex/replace_part/modifier
  2. (\d) and (\d+) in (\d)x(\d+) are two captured groupes (\1 and \2 respectively).

More examples here.

Jahid
  • 21,542
  • 10
  • 90
  • 108
  • Works like a charm, and it also shows the transformation of the file name before taking any action. <3 – ssi-anik Jun 28 '21 at 04:32
0

If you use rnr, the command would be:

rnr -f '.*(\d{1})x(\d{2}).*' 'S0${1}E${2}.str' *.srt

rnr has the benefit of being able to undo the command.

Ahmad Ismail
  • 11,636
  • 6
  • 52
  • 87