在 Linux 中查找多个文件并重命名它们

我在 Suse 10系统中有像 a_dbg.txt, b_dbg.txt ...这样的文件。我想编写一个 bash shell 脚本,通过从这些文件中删除“ _ dbg”来重命名这些文件。

Google 建议我使用 rename命令,所以我在 CURRENT_FOLDER上执行了 rename _dbg.txt .txt *dbg*命令

我的实际 CURRENT_FOLDER包含以下文件。

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

执行 rename命令后,

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

它不是递归执行,如何使这个命令重命名所有子目录中的文件。像 XXYY我将有这么多子目录的名称是不可预测的。而且我的 CURRENT_FOLDER也将有一些其他的文件也。

151305 次浏览

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.

with bash:

shopt -s globstar nullglob
rename _dbg.txt .txt **/*dbg*

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

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\")' '{}' \;

classic solution:

for f in $(find . -name "*dbg*"); do mv $f $(echo $f | sed 's/_dbg//'); done

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

For renaming recursively I use the following commands:

find -iname \*.* | rename -v "s/ /-/g"

You can use this below.

rename --no-act 's/\.html$/\.php/' *.html */*.html

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.

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

find -iname \*.* | grep oldname | rename -v "s/oldname/newname/g

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"