84

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
  • 42,211
  • 10
  • 116
  • 125
orezvani
  • 3,335
  • 8
  • 41
  • 53

8 Answers8

95

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
  • 42,211
  • 10
  • 116
  • 125
  • 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
  • 4
    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
36

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,322
  • 3
  • 26
  • 43
  • 4
    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
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
  • 3,402
  • 2
  • 19
  • 23
  • 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
11

Use mmv (mass-move?)

It's simple but useful: The * wildcard matches any string (without slashes) and ? matches any character in the string to be matched. Use #X in the replace string to refer to the X-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 made:

Friends - 6x?? - Tow *           .srt    matches
Friends - 6x03 - Tow Ross' Denial.srt    which is replaced by
            ↓↓
        S06E03.srt

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!

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

Community
  • 1
  • 1
xoxox
  • 669
  • 1
  • 13
  • 23
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
  • 500
  • 6
  • 8
6

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
  • 341
  • 3
  • 10
  • 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
5

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
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
  • 19,822
  • 8
  • 86
  • 102
  • 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