If you don't have rename (it's a really short Perl script) or your rename is the more simplistic util-linux version or you simply want to know how to make your command work: you just need to send it to the shell for execution:

find spec -name "*_test.rb" -exec sh -c 'echo mv "(echo "$1" | sed s/test.rb\$/spec.rb/)"' _ {} \;
Answer from Dennis Williamson on serverfault.com
Discussions

bash - Using sed to mass rename files - Stack Overflow
Objective Change these filenames: F00001-0708-RG-biasliuyda F00001-0708-CS-akgdlaul F00001-0708-VF-hioulgigl to these filenames: F0001-0708-RG-biasliuyda F0001-0708-CS-akgdlaul F0001-0708-VF-hioul... More on stackoverflow.com
๐ŸŒ stackoverflow.com
How to rename multiple files with sed?
Don't need sed, use bash. for i in *-tmp ; do mv "${i}" "${i/-tmp/.tmp}" ; done More on reddit.com
๐ŸŒ r/bash
14
6
March 18, 2022
shell - How can I recursively replace a string in file and directory names? - Unix & Linux Stack Exchange
I have a git clone of etckeeper, ... all files and directories with etckeeper in the name to usrkeeper. For example, ./foo-etckeeper-bar should be renamed to ./foo-usrkeeper-bar. ... However, I can't figure out how to actually do the renaming. I tried combining xargs with mv: % find . -path '*etckeeper*' -print0 | xargs -0 -n 1 -J % bash -c mv % '$(echo' % \| sed ... More on unix.stackexchange.com
๐ŸŒ unix.stackexchange.com
sed - how to rename multiple files by replacing string in file name? this string contains a "#" - Unix & Linux Stack Exchange
I apologize I copied and pasted your example without paying full attention, I did the rename command without the -n. I believe @DTK address the problem, I was not escaping the #. ... To replace # by somethingelse for filenames in the current directory (not recursive) you can use the (Perl-)rename ... More on unix.stackexchange.com
๐ŸŒ unix.stackexchange.com
Top answer
1 of 4
4

You could do:

find . -name '*\[M]\[@0]\[0].dav.avi' -type f -exec sh -c '
  for file do
    echo mv "$file" "${file%"[M][@0][0].dav.avi"}.avi"
  done' sh {} +

(remove echo if that looks all right).

Or if you want to rename all the .dav.avi to .avi, but also remove a [M][@0][0] if there:

find . -name '*.dav.avi' -type f -exec sh -c '
  for file do
    new=${file%.*.*}
    new=${new%"[M][@0][0]"}.avi
    echo mv "$file" "$new"
  done' sh +

Those are POSIX syntax and also work with busybox utilities so should work on synology.

A few notes (based on observations of your own and other answers):

  • the pattern passed to find -name must be quoted, otherwise it could be expanded by the shell.
  • you can't use xargs on file names unless you can guarantee that those file names won't contain blanks or newlines or quotes or backslashes.
  • Use printf instead of echo to output arbitrary data.
  • Leaving a variable expansion unquoted in list context has a very special meaning. You don't want to do it here.
  • The . character is a special regular expression operator, you need to escape it in regexps if you want it to be treated literally.
  • The syntax to read a line with read is IFS= read -r line, but again there's no guarantee that a file path be made of only one line.
  • It's often better to do (cd -P -- "$dir" && find . ... -exec) than find -- "$dir" ... -exec because it avoids problems with $dir values that happen to be find predicates or start with - and because it shortens the file paths passed to -exec and thus allows to pass more.
2 of 4
0

Here is a quick solution using sed to rename the files:

echo "10.01.07-10.01.48[M][@0][0].dav.avi" | sed 's/\[.*dav//'
10.01.07-10.01.48.avi

I know nothing about the Synology shell though, but I hope that sed is available!

Use (for example) in a loop:

#!/bin/sh

for file in *avi; do
    newName=$(echo "$file" | sed 's/\[.*dav//')
    mv -v "$file" "$newName" 
done

Before:

ls
09.01.07-13.11.48[M][@0][0].dav.avi
110.01.07-13.11.48[M][@0][0].dav.avi
12.01.07-13.11.48[M][@0][0].dav.avi

Running the script:

./renameAvi.sh 
โ€09.01.07-13.11.48[M][@0][0].dav.aviโ€ -> โ€09.01.07-13.11.48.aviโ€
โ€110.01.07-13.11.48[M][@0][0].dav.aviโ€ -> โ€110.01.07-13.11.48.aviโ€
โ€12.01.07-13.11.48[M][@0][0].dav.aviโ€ -> โ€12.01.07-13.11.48.aviโ€

After:

ls
09.01.07-13.11.48.avi   12.01.07-13.11.48.avi
110.01.07-13.11.48.avi 


Edit: If you want to use find one way to implement is like in this oneliner:

find . -name "*.avi" | while read fileName; do newName=$(echo "$fileName" | sed 's/\[.*dav//'); mv -v "$fileName" "$newName"; done
โ€./01.01.07-13.11.38[M][@0][0].dav.aviโ€ -> โ€./01.01.07-13.11.38.aviโ€
โ€./01.01.07-13.11.39[M][@0][0].dav.aviโ€ -> โ€./01.01.07-13.11.39.aviโ€

sed --version
sed (GNU sed) 4.2.2

find --version
find (GNU findutils) 4.4.2
Top answer
1 of 13
182

First, I should say that the easiest way to do this is to use the prename or rename commands.

On Ubuntu, OSX (Homebrew package rename, MacPorts package p5-file-rename), or other systems with perl rename (prename):

Copyrename s/0000/000/ F0000*

or on systems with rename from util-linux-ng, such as RHEL:

Copyrename 0000 000 F0000*

That's a lot more understandable than the equivalent sed command.

But as for understanding the sed command, the sed manpage is helpful. If you run man sed and search for & (using the / command to search), you'll find it's a special character in s/foo/bar/ replacements.

Copy  s/regexp/replacement/
         Attempt  to match regexp against the pattern space.  If successโ€
         ful,  replace  that  portion  matched  with  replacement.    The
         replacement may contain the special character & to refer to that
         portion of the pattern space  which  matched,  and  the  special
         escapes  \1  through  \9  to refer to the corresponding matching
         sub-expressions in the regexp.

Therefore, \(.\) matches the first character, which can be referenced by \1. Then . matches the next character, which is always 0. Then \(.*\) matches the rest of the filename, which can be referenced by \2.

The replacement string puts it all together using & (the original filename) and \1\2 which is every part of the filename except the 2nd character, which was a 0.

This is a pretty cryptic way to do this, IMHO. If for some reason the rename command was not available and you wanted to use sed to do the rename (or perhaps you were doing something too complex for rename?), being more explicit in your regex would make it much more readable. Perhaps something like:

Copyls F00001-0708-*|sed 's/F0000\(.*\)/mv & F000\1/' | sh

Being able to see what's actually changing in the s/search/replacement/ makes it much more readable. Also it won't keep sucking characters out of your filename if you accidentally run it twice or something.

2 of 13
61

You've had your sed explanation. Now you can use just the shell. No need for external commands.

Copyfor file in F0000*
do
    echo mv "$file" "${file/#F0000/F000}"
    # ${file/#F0000/F000} means replace the pattern that starts at beginning of string
done

Note that this snippet runs echo as a safety measure that prints what the mv command will do without doing it. To actually perform the mv, you need to remove echo.

๐ŸŒ
Linux Tutorials
linuxtutorials.org โ€บ linux-rename-multiple-files-at-once
How to rename multiple files at once in Linux | Linux Tutorials
October 10, 2021 - Using find and sed j@ubuntu2004:~/tmp/1010$ ls 1.html 2.html 3.html j@ubuntu2004:~/tmp/1010$ for f in *.html;do mv $f $(echo $f |sed s/.html/.htm/);done j@ubuntu2004:~/tmp/1010$ ls 1.htm 2.htm 3.htm The benefit of using find is you can rename multiple files recursively or traversing directories.
๐ŸŒ
Commandlinefu
commandlinefu.com โ€บ commands โ€บ view โ€บ 8368 โ€บ bulk-rename-files-with-sed-one-liner
bulk rename files with sed, one-liner Using ls, sed, xargs
April 30, 2011 - ls * | sed -e 'p;s/foo/bar/' | xargs -n2 mv - (bulk rename files with sed, one-liner Renames all files in a directory named foo to bar. foobar1 gets renamed to barbar1 barfoo2 gets renamed to barbar2 fooobarfoo gets renamed to barobarfoo NOTE: Will break for files with spaces AND new lines AND for an empty expansion of the glob '*'). The best command line collection on the internet, submit yours and save your favorites.
๐ŸŒ
Reddit
reddit.com โ€บ r/bash โ€บ how to rename multiple files with sed?
r/bash on Reddit: How to rename multiple files with sed?
March 18, 2022 -
  1. Sample files as follow; I just want to rename -tmp to .tmp

$ ls -1 *tmp
file-3-tmp
file-4-tmp

2. 1st attempt with sed

$ for i in *tmp; do echo $i | sed 's/-t/.t/'; done
file-3.tmp
file-4.tmp

Looks promising, but this is just echo, not the actual file. Same output as step 1. So I use sed -i but still didn't work

$ for i in *tmp; do sed -i 's/-t/.t/' $i; done
$ ls -1 *tmp
file-3-tmp
file-4-tmp

Let me know the right way to do this. Thanks

Find elsewhere
Top answer
1 of 3
5

You can use the rename command (see edit 1).

Solution 1

For a reasonable number of files/directory, by setting bash 4 option globstar (not works on recursive name, see edit 3):

shopt -s globstar
rename -n 's/etckeeper/userkeeper/g' **

Solution 2

For a big number of files/directories using rename and find in two steps to prevent failed rename on files in just renamed directories (see edit 2):

find . -type f -exec rename 's/etckeeper/userkeeper/g' {} \;
find . -type d -exec rename 's/etckeeper/userkeeper/g' {} \;

EDIT 1:

There are two different rename commands. This answer uses the Perl-based rename command, available in Debian-based distros. To have it on a not Debian based distro you can install from cpan or grab it around.

EDIT 2:

As pointed out by Jeff Schaller in the comments the -depth find option Process each directory's contents before the directory itself so only an "ordered" find by -depth option would be enough:

find . -depth -exec rename 's/etckeeper/userkeeper/g' {} \;

EDIT 3

Solution 1 doesn't work for recursive rename targets, (es. etckeeper/etckeeper/etckeeper) becasue outer levels are processed before inner levels and pointer to inner levels become useless. (After the first rename etckeeper/etckeeper/etckeeper will be named usrkeeper/etckeeper/etckeeper so the rename for etckeeper/etckeeper/ and etckeeper/etckeeper/etckeeper will fail). The same problem fixed in find by -depth options.

EDIT4

As pointed out in the comments by cas, I'd use {} + rather than {} \; - forking a perl script like rename multiple times (once per file/dir) is expensive.

2 of 3
1

Your original question is actually pretty easy to answer: xargs (at least on OS X) has an -I option, too, which does not require that the replacement string be a unique argument. So, the invocation simply becomes:

% find . -path '*etckeeper*' -print0 | xargs -0 -n 1 -I % bash -c 'echo mv % $(echo % | sed "s/etckeeper/usrkeeper/g" )'

Easy peasy. Now, let's rename in the correct order. As it turns out, find will print directories first (because it has to process them before it descends into them), so all we need to do is reverse the order of the filenames:

% find . -path '*etckeeper*' -print | tail -r | xargs -n 1 -I % bash -c 'echo mv % $(echo % | sed "s/etckeeper/usrkeeper/g" )'

Note that I've switched find -print0 | xargs -0 for find -print | xargs. Why? Because tail -r reverses based on newlines, not null characters. If you didn't switch it, tail -r would print out the same thing you piped into it! So the script is more correct now, but also it will break on filenames that contain e.g. newlines.

Note that if you don't have tail -r, you should try tac. See: How can I reverse the order of lines in a file? on Stack Overflow.

Now, the issue is that the sed command is too aggressive. For example, with a tree like:

.
โ”œโ”€โ”€ etckeeper-foo
โ”‚   โ””โ”€โ”€ etckeeper-bar.md
โ””โ”€โ”€ some-other-directory
    โ””โ”€โ”€ etckeeper-baz.md

You'll get mv invocations that look like:

mv ./some-other-directory/etckeeper-baz.md ./some-other-directory/usrkeeper-baz.md
mv ./etckeeper-foo/etckeeper-bar.md ./usrkeeper-foo/usrkeeper-bar.md
mv ./etckeeper-foo ./usrkeeper-foo

The first one's fine, but the second one's clearly not - since we haven't yet done mv ./etckeeper-foo ./usrkeeper-foo!

Let's only replace in the last pathname component. We can do this by using basename and dirname:

% find . -name '*etckeeper*' -print | tail -r | xargs -n 1 -I % bash -c 'echo mv % $(dirname %)/$(basename % | sed "s/etckeeper/usrkeeper/g" )'

Note that the other change I've made is using find -name, not find -path.

This produces the correct mv invocations:

mv ./some-other-directory/etckeeper-baz.md ./some-other-directory/usrkeeper-baz.md
mv ./etckeeper-foo/etckeeper-bar.md ./etckeeper-foo/usrkeeper-bar.md
mv ./etckeeper-foo ./usrkeeper-foo

Finally. Remember, once again, that this will fail on esoteric filenames. Note also that if your filenames are particularly long, xargs will not work properly because the arguments to mv become too long. You can (at least on OS X) pass -x to xargs to tell it to immediately fail if this is the case.

๐ŸŒ
Super User
superuser.com โ€บ questions โ€บ 1467293 โ€บ rename-files-with-sed-rearanging-filenames
bash - rename files with sed (rearanging filenames) - Super User
funny story - I have openSUSE and this command is not working with latest rename version..., probably it's a good answer but how to make it work (I assume that sed will be used) ... Make sure it's the right rename. man rename should give Larry Wall as the AUTHOR. Otherwise try prename or perl-rename. ... Do cd into the folder holding the files.
๐ŸŒ
Unix.com
unix.com โ€บ shell programming and scripting
Renaming files with sed - Shell Programming and Scripting - Unix Linux Community
November 18, 2017 - Hi all, I created file like this AAb.lol AAc.lol AAx.lol test.sh My goal is to create a script (test.sh) which renames all the files to their original name without AA. I want to end up with this: b.lol c.lol x.lol Using sed how is it possible? i tried to write the script #!/bin/bash for i in $( ls ); do NewName='sed' 's/AA//g' '$i' mv '$i' $NewName done The output however is a lot of times this: From that 2nd line i can tell that $NewName is just empty.
Top answer
1 of 4
10

If your structure only has two levels, you don't need to use recursion.

Don't parse filenames with sed. If you want to use regex and sed style syntax to rename files, use rename. If you're using Ubuntu 17.10, you need to install it

sudo apt install rename

Then you can use pa4080's answer.

With rename, use the -n option for testing.

rename -n 'expression' file(s)

You could also just use the mv command. As a one-line command:

for d in ./*/; do mv -v "$d" "${d/Edition/Volume}"; done; for f in ./*/*; do mv -v "$f" "${f/Edition/Volume}"; done

You can use echo for testing with mv, ie echo mv -v source dest, but it gives inaccurate results here, unless you test and then run the loops separately.

As a script:

#!/bin/bash

# rename directories first
for d in ./*/; do
    mv -v "$d" "${d/Edition/Volume}"
done

# now rename files
for f in ./*/*; do
    mv -v "$f" "${f/Edition/Volume}"
done

mv and rename recognise -- to indicate the end of options. Since all paths begin with ./ in this example, we do not need to use that, but if paths may begin with -, then use -- at the end of options, eg rename -n -- 's/foo/bar/' *, to prevent those paths being interpreted as options.

2 of 4
7

You could use the command rename two times to accomplish this task:

rename 's/Edition/Volume/' */        # rename the directories
rename 's/Edition/Volume/' */*.pdf   # rename the PDF files inside

Here are two similar questions:

  • Recursive bash script
  • Explaining a shell script to recursively print full directory tree
Top answer
1 of 6
17

Note that you're using the Perl script called rename distributed by Debian and derivatives (Ubuntu, Mint, โ€ฆ). Other Linux distributions ship a completely different, and considerably less useful, command called rename.

y/A-Z/a-z/ translates each character in the range A through Z into the corresponding character in the range a through z, i.e. ASCII uppercase letters to the corresponding lowercase letter. To perform the opposite translation, use y/a-z/A-Z/. Another way to write the same command is rename '$_ = uc($_)' * โ€” uc is the uppercase function, and the rename command renames files based on the transformation made to the $_ variable.

rename 'โ€ฆ' * only renames files in the current directory, because that's what * matches. Dot files (files whose name begins with .) are skipped, too.

If you want to rename files in the current directory and in subdirectories recursively, you can use the find command to traverse the current directory recursively. There is a difficulty here: if you call rename, this renames both the directory and the base name part. If you call rename on a directory before recursing into it (find -exec rename โ€ฆ {} \;), find gets confused because it's found a directory but that directory no longer exists by the time it tries to descend into it. You can work around this by telling find to traverse a directory before acting on it, but then you end up attempting to rename foo/bar to FOO/BAR but the directory FOO doesn't exist.

A simple way to avoid this difficulty is to make the renaming command act only on the base name part of the path. The regular expression ([^/]*\Z) matches the final part of the path that doesn't contain a /.

find . -depth -exec rename 's!([^/]*\Z)!uc($1)!e' {} +

The shell zsh provides more convenient features for renaming โ€” even more cryptic than Perl, but terser and often easier to compose.

The function zmv renames files based on patterns. Run autoload -U zmv once to activate it (put this line in your .zshrc).

In the first argument to zmv (the pattern to replace), you can use zsh's powerful wildcard patterns. In the second argument to zmv (the replacement text), you can use its parameter expansion features, including history modifiers.

zmv -w '**/*' '$1$2:u'

Explanation:

  • -w โ€” automatic assign numeric variables to each wildcard pattern
  • **/* โ€” all files in subdirectories, recursively (**/ matches 0, 1 or more levels of subdirectories)
  • $1 โ€” the first numeric variable, here matching the directory part of each path
  • $2:u โ€” the second numeric variable, here matching the base name part of each path, with the :u modifier to convert the value to uppercase

As an added bonus, this respects the ambient locale settings.

If you aren't sure about a zmv command you wrote, you can pass the -n option to print what the command would do and not change anything. Check the output, and if it does what you want, re-run the command without -n to actually act.

2 of 6
8

I'd like to direct anyone who's still being linked to this answer to the excellent answer Guiles Quernot gave to this question which doesn't require find.

The resulting command would be:

shopt -s globstar
rename -n 'y/a-z/A-Z/' **

But before running please read the answer linked for caveats regarding old bash versions.

Finally in case someone is wondering what does the y/// command does in perl regex. Here's a link to the relevant documentation.

๐ŸŒ
Mikkel Paulson
mikkel.ca โ€บ blog โ€บ three-ways-to-bulk-rename
Three ways to bulk rename files | Mikkel Paulson
June 18, 2021 - $ ls | sed -e 's/File 0*([1-9][0-9]*)/mv -v "\0" "I was \1 and now I am \0"/' | bash renamed 'File 001' -> 'I was 1 and now I am File 001' renamed 'File 002' -> 'I was 2 and now I am File 002' renamed 'File 003' -> 'I was 3 and now I am File 003' renamed 'File 004' -> 'I was 4 and now I am ...