在 linux 中使用正则表达式重命名文件

我有一套文件名为:

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

我想像下面这样重命名它们

S06E03.srt
S06E20.srt
S06E05.srt

我应该怎样做才能在 linux 终端中完成这项工作? 我已经安装了重命名,但是您使用以下命名方法得到了错误:

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

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.

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.

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.

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

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.

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!

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)

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

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.