106

I am having files like a_dbg.txt, b_dbg.txt ... in a Suse 10 system. I want to write a bash shell script which should rename these files by removing "_dbg" from them.

Google suggested me to use rename command. So I executed the command rename _dbg.txt .txt *dbg* on the CURRENT_FOLDER

My actual CURRENT_FOLDER contains the below files.

CURRENT_FOLDER/a_dbg.txt
CURRENT_FOLDER/b_dbg.txt
CURRENT_FOLDER/XX/c_dbg.txt
CURRENT_FOLDER/YY/d_dbg.txt

After executing the rename command,

CURRENT_FOLDER/a.txt
CURRENT_FOLDER/b.txt
CURRENT_FOLDER/XX/c_dbg.txt
CURRENT_FOLDER/YY/d_dbg.txt

Its not doing recursively, how to make this command to rename files in all subdirectories. Like XX and YY I will be having so many subdirectories which name is unpredictable. And also my CURRENT_FOLDER will be having some other files also.

jww
  • 97,681
  • 90
  • 411
  • 885
rashok
  • 12,790
  • 16
  • 88
  • 100

13 Answers13

158

You can use find to find all matching files recursively:

find . -iname "*dbg*" -exec rename _dbg.txt .txt '{}' \;

EDIT: what the '{}' and \; are?

The -exec argument makes find execute rename for every matching file found. '{}' will be replaced with the path name of the file. The last token, \; is there only to mark the end of the exec expression.

All that is described nicely in the man page for find:

 -exec utility [argument ...] ;
         True if the program named utility returns a zero value as its
         exit status.  Optional arguments may be passed to the utility.
         The expression must be terminated by a semicolon (``;'').  If you
         invoke find from a shell you may need to quote the semicolon if
         the shell would otherwise treat it as a control operator.  If the
         string ``{}'' appears anywhere in the utility name or the argu-
         ments it is replaced by the pathname of the current file.
         Utility will be executed from the directory from which find was
         executed.  Utility and arguments are not subject to the further
         expansion of shell patterns and constructs.
SKPS
  • 5,433
  • 5
  • 29
  • 63
kamituel
  • 34,606
  • 6
  • 81
  • 98
  • @kamituel Thanks. I am learning linux and used find few times I was wondering what those meant. thanks for nice explanation. Can You please tell what -iname does? and can we use the above command(In Centos 7) as `find . -type f "*dbg*" -exec rename "_dbg.txt" ".txt" * {} \;` – Syed Mudabbir Feb 25 '16 at 16:07
  • 2
    i checked it and it works only on files not on directories. – Syed Mudabbir Feb 25 '16 at 16:17
  • I got this error multiple times: `Illegal octal digit '9' at (user-supplied code), at end of line`. I've tried both single quotes, double quotes and no quotes for the arguments of `rename`. Specifically I'm trying to replace `0123-MISC` with `0123_AB_MISC`. this is my command `find . -iname "*0123-MISC*" -exec rename 0123-MISC 0123_AB_MISC {} \;` – Eddy May 16 '17 at 08:10
  • Turns out that the issue is that numbers with a leading `0` are being interpreted as octal digits by perl, which is the scripting language used by `rename`. In this case I had to go for the solution by @glenn – Eddy May 16 '17 at 08:28
  • 18
    For those getting `Bareword "..." not allowed while "strict subs" in use at ...`, try: `find . -iname "*BUILD*" -exec rename 's/_dbg.txt/.txt/' '{}' \;` – jsand Feb 23 '18 at 22:52
  • Worked great for me. I used it to rename all .htaccess files in a directory so I could test site functionality for a PHP upgrade without permissions getting in the way: ```find . -iname ".htaccess" -exec rename .htaccess .htaccess_disabled '{}' \;``` And to revert: ```find . -iname ".htaccess_disabled" -exec rename .htaccess_disabled .htaccess '{}' \;``` – mmseng Feb 05 '19 at 23:23
  • from manual find. -iname pattern. Like -name, but the match is case insensitive. For example, the patterns `fo*' and `F??' match the file names `Foo', `FOO', `foo', `fOo', etc. The pattern `*foo*` will also match a file called '.foobar'. – user43326 Feb 29 '20 at 10:07
  • 9
    I get `find: rename: No such file or directory` – Bartek Pacia Aug 02 '20 at 16:25
  • 4
    @BartekPacia `rename` is a unix util (check https://www.gnu.org/software/gnulib/manual/html_node/rename.html). The error suggests it is not installed on your machine. For macOS (which is typically the case) you can address this with `brew install rename`. – vervas Jul 15 '21 at 08:05
  • @jsand, thanks for the comment, but what does `"*BUILD*"` mean? – abc Nov 23 '22 at 08:48
  • On MaxOSX i'm getting `syntax error at (eval 2) line 1, near "; ."` – Ricky Levi Jul 31 '23 at 10:03
29

For renaming recursively I use the following commands:

find -iname \*.* | rename -v "s/ /-/g"
ρяσѕρєя K
  • 132,198
  • 53
  • 198
  • 213
Gantan
  • 301
  • 3
  • 4
  • Also useful to restore backup files. `find -iname \*.* | rename -v -f "s/\.bak//g"` – Paolo Dec 07 '17 at 19:55
  • My rename does not have -v. Are you on Mac OS X? I'm Ubuntu 16. – Katastic Voyage Mar 27 '18 at 08:21
  • -v is equivalent to --verbose on my release of the 'rename' tool; not important regarding the result. Please enter 'man rename' for further clarification. – Gantan Mar 28 '18 at 08:58
  • 5
    This does not work for me on Ubuntu 18.10 because `rename` tries to rename the entire path and fails if a parent directory would be changed. You must instead operate on basenames only as shown at: https://stackoverflow.com/questions/16541582/find-multiple-files-and-rename-them-in-linux/54163971#54163971 and go depth first with `-depth`. – Ciro Santilli OurBigBook.com Apr 25 '19 at 06:54
  • the default -f is also force, not find – Scott Sep 18 '20 at 15:58
  • 1
    @CiroSantilliПутлерКапут六四事 exactly that. – MrR Apr 26 '22 at 20:00
  • Watch out, on some linux releases rename is a util-linux simple string replacement Usage: rename [options] expression replacement file..., on others rename is a perl tool (as in this example) with more complex expression replace https://learnbyexample.github.io/learn_perl_oneliners/perl-rename-command.html If your rename doesn't have the -v or -n option is is not the perl rename. – gaoithe Jul 18 '22 at 11:57
22

small script i wrote to replace all files with .txt extension to .cpp extension under /tmp and sub directories recursively

#!/bin/bash

for file in $(find /tmp -name '*.txt')
do
  mv $file $(echo "$file" | sed -r 's|.txt|.cpp|g')
done
Alok Singhal
  • 93,253
  • 21
  • 125
  • 158
VishnuVardhanA
  • 587
  • 5
  • 6
  • 7
    Broken method! please don't do `for file in $(find ...)`. See [Why you shouldn't parse the output of ls(1)](http://mywiki.wooledge.org/ParsingLs). – gniourf_gniourf Sep 17 '15 at 18:34
15

with bash:

shopt -s globstar nullglob
rename _dbg.txt .txt **/*dbg*
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • 5
    Great solution. I had to enable those bash options with `shopt -s globstar nullstar`. For the rename tool that is installed via `brew install rename` on MacOS, I had to do `rename -s oldStuff newStuff **/*` – Xander Dunn Mar 04 '15 at 21:44
13

find -execdir rename also works for non-suffix replacements on basenames

https://stackoverflow.com/a/16541670/895245 works directly only for suffixes, but this will work for arbitrary regex replacements on basenames:

PATH=/usr/bin find . -depth -execdir rename 's/_dbg.txt$/_.txt' '{}' \;

or to affect files only:

PATH=/usr/bin find . -type f -execdir rename 's/_dbg.txt$/_.txt' '{}' \;

-execdir first cds into the directory before executing only on the basename.

Tested on Ubuntu 20.04, find 4.7.0, rename 1.10.

Convenient and safer helper for it

find-rename-regex() (
  set -eu
  find_and_replace="$1"
  PATH="$(echo "$PATH" | sed -E 's/(^|:)[^\/][^:]*//g')" \
    find . -depth -execdir rename "${2:--n}" "s/${find_and_replace}" '{}' \;
)

GitHub upstream.

Sample usage to replace spaces ' ' with hyphens '-'.

Dry run that shows what would be renamed to what without actually doing it:

find-rename-regex ' /-/g'

Do the replace:

find-rename-regex ' /-/g' -v

Command explanation

The awesome -execdir option does a cd into the directory before executing the rename command, unlike -exec.

-depth ensure that the renaming happens first on children, and then on parents, to prevent potential problems with missing parent directories.

-execdir is required because rename does not play well with non-basename input paths, e.g. the following fails:

rename 's/findme/replaceme/g' acc/acc

The PATH hacking is required because -execdir has one very annoying drawback: find is extremely opinionated and refuses to do anything with -execdir if you have any relative paths in your PATH environment variable, e.g. ./node_modules/.bin, failing with:

find: The relative path ‘./node_modules/.bin’ is included in the PATH environment variable, which is insecure in combination with the -execdir action of find. Please remove that entry from $PATH

See also: https://askubuntu.com/questions/621132/why-using-the-execdir-action-is-insecure-for-directory-which-is-in-the-path/1109378#1109378

-execdir is a GNU find extension to POSIX. rename is Perl based and comes from the rename package.

Rename lookahead workaround

If your input paths don't come from find, or if you've had enough of the relative path annoyance, we can use some Perl lookahead to safely rename directories as in:

git ls-files | sort -r | xargs rename 's/findme(?!.*\/)\/?$/replaceme/g' '{}'

I haven't found a convenient analogue for -execdir with xargs: https://superuser.com/questions/893890/xargs-change-working-directory-to-file-path-before-executing/915686

The sort -r is required to ensure that files come after their respective directories, since longer paths come after shorter ones with the same prefix.

Tested in Ubuntu 18.10.

Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
  • 5
    Works for me. This solution should be voted higher. – Paul Chris Jones Dec 09 '19 at 16:09
  • One improvement: do not attempt a rename if the file doesn't match - i.e. pass the "match" part as a separate parameter and use it as a param to find with `-iname` – MrR Apr 26 '22 at 21:21
  • 1
    Great answer! It handles also directory renaming properly. I just would like to suggest a way to avoid setting the `PATH=/usr/bin` workaround. Just give the full path when calling rename: `find . -depth -name "cnn4*" -execdir /usr/bin/rename 's/cnn4/cnn5/' '{}' \;` – Helder Daniel Aug 20 '22 at 17:40
  • @HelderDaniel the `PATH` hacking is a workaround for people having relative paths on their `PATH` (arguably a bad idea, but was my case) see `node_modules` mention. It is hard to decide sometimes if one should give the "works no matter what" or "works in sane conditions" version of commands more proeminenty. – Ciro Santilli OurBigBook.com Aug 20 '22 at 19:20
  • 1
    @CiroSantilliПутлерКапут六四事, Yeah, I got it. I was just suggesting another way to deal with problems with the path. If we specify the full path we do not need to worry about the value of the PATH env var. But ok, it is the same actually. We have the specify the full path whether in the PATH var or before the command – Helder Daniel Aug 26 '22 at 11:32
6

Script above can be written in one line:

find /tmp -name "*.txt" -exec bash -c 'mv $0 $(echo "$0" | sed -r \"s|.txt|.cpp|g\")' '{}' \;
Manh Tai
  • 356
  • 4
  • 8
  • 1
    @steven try using this find . -name "*.txt" -exec bash -c 'mv $0 $(echo "$0" | sed -r "s/.txt/.cpp/g")' '{}' \; – xxbinxx Sep 20 '17 at 06:11
  • 1
    Opening / executing a new shell for every file is extremely expensive (slow), so I usually don't do that. – Binarus Jan 06 '19 at 09:47
4

If you just want to rename and don't mind using an external tool, then you can use rnm. The command would be:

#on current folder
rnm -dp -1 -fo -ssf '_dbg' -rs '/_dbg//' *

-dp -1 will make it recursive to all subdirectories.

-fo implies file only mode.

-ssf '_dbg' searches for files with _dbg in the filename.

-rs '/_dbg//' replaces _dbg with empty string.

You can run the above command with the path of the CURRENT_FOLDER too:

rnm -dp -1 -fo -ssf '_dbg' -rs '/_dbg//' /path/to/the/directory
Jahid
  • 21,542
  • 10
  • 90
  • 108
1

You can use this below.

rename --no-act 's/\.html$/\.php/' *.html */*.html
井上智文
  • 1,905
  • 17
  • 14
1

This command worked for me. Remember first to install the perl rename package:

find -iname \*.* | grep oldname | rename -v "s/oldname/newname/g
MrR
  • 411
  • 5
  • 12
Pirooz Jenabi
  • 440
  • 5
  • 7
1

In case anyone is comfortable with fd and rnr, the command is:

fd -t f -x rnr '_dbg.txt' '.txt'

rnr only command is:

rnr -f -r '_dbg.txt' '.txt' *

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

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

To expand on the excellent answer @CiroSantilliПутлерКапут六四事 : do not match files in the find that we don't have to rename.

I have found this to improve performance significantly on Cygwin.

Please feel free to correct my ineffective bash coding.

FIND_STRING="ZZZZ"
REPLACE_STRING="YYYY"

FIND_PARAMS="-type d"

find-rename-regex() (
  set -eu
  find_and_replace="${1}/${2}/g"
  echo "${find_and_replace}"
  find_params="${3}"
  mode="${4}"
  if [ "${mode}" = 'real' ]; then
    PATH="$(echo "$PATH" | sed -E 's/(^|:)[^\/][^:]*//g')" \
      find . -depth -name "*${1}*" ${find_params} -execdir rename -v "s/${find_and_replace}" '{}' \;
  elif [ "${mode}" = 'dryrun' ]; then
    echo "${mode}"
    PATH="$(echo "$PATH" | sed -E 's/(^|:)[^\/][^:]*//g')" \
      find . -depth -name "*${1}*" ${find_params} -execdir rename -n "s/${find_and_replace}" '{}' \;
  fi
)

find-rename-regex "${FIND_STRING}" "${REPLACE_STRING}" "${FIND_PARAMS}" "dryrun"
# find-rename-regex "${FIND_STRING}" "${REPLACE_STRING}" "${FIND_PARAMS}" "real"
MrR
  • 411
  • 5
  • 12
0

On Ubuntu (after installing rename), this simpler solution worked the best for me. This replaces space with underscore, but can be modified as needed.

find . -depth | rename -d -v -n "s/ /_/g"

The -depth flag is telling find to traverse the depth of a directory first, which is good because I want to rename the leaf nodes first.

The -d flag on rename tells it to only rename the filename component of the path. I don't know how general the behavior is but on my installation (Ubuntu 20.04), it could be the file or the directory as long as it is the leaf node of the path.

I recommend the -n (no action) flag first along with -v, so you can see what would get renamed and how.

Using the two flags together, it renames all the files in a directory first and then the directory itself. Working backwards. Which is exactly what I needed.

Dr Phil
  • 430
  • 5
  • 17
  • For what it's worth, there are multiple utilities called `rename`, with different features and different syntax. [The Ubuntu `rename` package](https://packages.ubuntu.com/search?keywords=rename) is the "Perl" one. (This is thus basically a duplicate of earlier answers with the same recommendation.) – tripleee Mar 13 '23 at 06:22
-4

classic solution:

for f in $(find . -name "*dbg*"); do mv $f $(echo $f | sed 's/_dbg//'); done
pulse
  • 303
  • 4
  • 18
  • 4
    This is completely broken. Please, don't do this. – gniourf_gniourf Sep 17 '15 at 18:15
  • `~/tmp$ mkdir a && cd a/ && touch 1_dbg.txt && cd ..` `~/tmp$ for f in $(find . -name "*dbg*"); do mv $f $(echo $f | sed 's/_dbg//'); done` `~/tmp$ cd a/ ~/tmp/a$ l total 8 drwxrwxr-x 2 repu1sion repu1sion 4096 Sep 17 21:17 . drwxrwxr-x 3 repu1sion repu1sion 4096 Sep 17 21:17 .. -rw-rw-r-- 1 repu1sion repu1sion 0 Sep 17 21:17 1.txt` – pulse Sep 17 '15 at 18:19
  • 1
    Maybe it _seems_ to work, but it breaks with filenames that contain spaces or glob characters. You say it's a _classic_ solution, but it's more likely the solution you came up with, yet it shows terrible mistakes (sorry to be harsh, but your line could be included in textbooks as an example of what not to do; exercise: count the number of mistakes). – gniourf_gniourf Sep 17 '15 at 18:23
  • 1
    `for f in $(find ....)`: don't do this! it breaks with filenames containing spaces or glob characters; besides, `find` has the `-exec` option that can very likely be used instead; or `for f in *_dbg*; do` or if you want a recursive glob, `for f in **/*_dbg; do` with `shopt -s globstar`. – gniourf_gniourf Sep 17 '15 at 18:27
  • `mv $f ...`: lacks quotes (otherwise, it breaks with filenames containing spaces or glob characters). Use `mv "$f" ...` instead, or even better `mf -- "$f" ...`. – gniourf_gniourf Sep 17 '15 at 18:28
  • `echo $f | sed 's/_dbg//'`: this typically shows a lack of knowledge of the parameter expansions capabilities of the shell (besides the lack of quotes in the `echo $f` statement). Consider instead `echo "${f/_dbg}"`. – gniourf_gniourf Sep 17 '15 at 18:30
  • 2
    Finally, a much better solution is: `shopt -s globstar nullglob; for f in **/*_dbg*; do mv -- "$f" "${f/_dbg}"; done`. – gniourf_gniourf Sep 17 '15 at 18:31
  • This should work: for f in `find . -regex "*dbg*"`; do mv -v $f $(echo $f | sed "s/_dbg//") ; done – Pooya Oct 02 '17 at 22:10