Differences between revisions 3 and 6 (spanning 3 versions)
Revision 3 as of 2007-11-30 03:00:29
Size: 3694
Editor: GreyCat
Comment: change internal links
Revision 6 as of 2008-06-30 14:14:16
Size: 4764
Editor: GreyCat
Comment: note GNU/BSD find requirement
Deletions are marked like this. Additions are marked like this.
Line 6: Line 6:
You can do mass renames in POSIX shells with [:BashFAQ#faq73:Parameter Expansion], like this: You can do mass renames with [:BashFAQ/073:Parameter Expansion], like this:
Line 9: Line 9:
# POSIX
Line 15: Line 16:
# Bash
Line 18: Line 20:
This invokes the external command {{{mv}}} once for each file, so it may not be as efficient as some of the {{{rename}}} implementations. This invokes the external command {{{mv(1)}}} once for each file, so it may not be as efficient as some of the {{{rename}}} implementations.
Line 20: Line 22:
If you want to do it recursively, then it becomes much more challenging. This example for renaming {{{*.foo}}} to {{{*.bar}}} works (in ["BASH"]) as long as no files have newlines in their names: If you want to do it recursively, then it becomes much more challenging. This example renames {{{*.foo}}} to {{{*.bar}}}:
Line 23: Line 25:
find . -name '*.foo' -print | while IFS=$'\n' read -r f; do # Bash
# Also requires GNU or BSD find(1)
find . -name '*.foo' -print0 | while read -r -d $'\0' f; do
Line 28: Line 32:
For more techniques on dealing with files with inconvenient characters in their names, see [:BashFAQ#faq20:FAQ #20]. For more techniques on dealing with files with inconvenient characters in their names, see [:BashFAQ/020:FAQ #20].
Line 30: Line 34:
To convert filenames to lower case: The trickiest part of recursive renames is ensuring that you do not change the directory component of a pathname, because something like this is doomed to failure:

{{{
mv "./FOO/BAR/FILE.TXT" "./foo/bar/file.txt"
}}}

Therefore, any recursive renaming command should only change the filename component of each pathname. If you need to rename the directories as well, those should be done separately. Furthermore, recursive directory renaming should either be done depth-first (changing only the last component of the directory name in each instance), or in several passes. Depth-first works better in the general case.

To convert filenames to lower case, if you have the utility {{{mmv(1)}}} on your machine, you could simply do:

{{{
# convert all filenames to lowercase
mmv "*" "#l1"
}}}

Otherwise, {{{tr(1)}}} may be helpful:
Line 34: Line 53:

for file in *
# POSIX
for file in "$@"
Line 62: Line 81:
Or, if you have the utility {{{mmv(1)}}} on your machine, you could simply do:

{{{
# convert all filenames to lowercase
mmv "*" "#l1"
}}}

This technique can also be used to replace all unwanted characters in a file name e.g. with '_' (underscore). The script is the same as above, only the "newname=..." line has changed.
This technique can also be used to replace all unwanted characters in a file name, e.g. with '_' (underscore). The script is the same as above, with only the "newname=..." line changed.
Line 73: Line 85:
# Portable version.
for file in *
# POSIX
for file in "$@"
Line 77: Line 89:
    newname=$(echo "$file" | sed 's/[^a-zA-Z0-9_.]/_/g')     newname=$(echo "$file" | sed 's/[^[:alnum:]_.]/_/g')
Line 84: Line 96:
The character class in {{{[]}}} contains all allowed characters; modify it as needed. The character class in {{{[]}}} contains all the characters we want to keep (after the {{{^}}}); modify it as needed. The {{{[:alnum:]}}} range stands for all the letters and digits of the current locale.
Line 86: Line 98:
Here's an example that does the same thing, but this time using Parameter Expansion instead: Here's an example that does the same thing, but this time using Parameter Expansion instead of {{{sed}}}:
Line 90: Line 102:
for file in *; do # Bash
for file in "$@"; do
Line 92: Line 105:
   newname=${f//[^a-zA-Z0-9_.]/_}    newname=${f//[^[:alnum:]_.]/_}
Line 98: Line 111:

It should be noted that all three of these examples contain a [:RaceCondition:race condition] -- an existing file could be overwritten if it is created in between the `[ -f "$newname" ...` and `mv "$file" ...` commands. Solving this issue is beyond the scope of this page, however.

Anchor(faq30)

How can I rename all my *.foo files to *.bar, or convert spaces to underscores, or convert upper-case file names to lower case?

Some GNU/Linux distributions have a rename(1) command, which you can use for the former; however, the syntax differs from one distribution to the next, so it's not a portable answer. Consult your system's man pages if you want to learn how to use yours, if you have one at all. It's often perfectly good for one-shot interactive renames, just not in portable scripts. We don't include any rename(1) examples here because it's too confusing -- there are two common versions of it and they're totally incompatible with each other.

You can do mass renames with [:BashFAQ/073:Parameter Expansion], like this:

# POSIX
for f in *.foo; do mv "$f" "${f%.foo}.bar"; done

Here's a similar example, this time replacing spaces in filenames with underscores:

# Bash
for f in *\ *; do mv "$f" "${f// /_}"; done

This invokes the external command mv(1) once for each file, so it may not be as efficient as some of the rename implementations.

If you want to do it recursively, then it becomes much more challenging. This example renames *.foo to *.bar:

# Bash
# Also requires GNU or BSD find(1)
find . -name '*.foo' -print0 | while read -r -d $'\0' f; do
  mv "$f" "${f%.foo}.bar"
done

For more techniques on dealing with files with inconvenient characters in their names, see [:BashFAQ/020:FAQ #20].

The trickiest part of recursive renames is ensuring that you do not change the directory component of a pathname, because something like this is doomed to failure:

mv "./FOO/BAR/FILE.TXT" "./foo/bar/file.txt"

Therefore, any recursive renaming command should only change the filename component of each pathname. If you need to rename the directories as well, those should be done separately. Furthermore, recursive directory renaming should either be done depth-first (changing only the last component of the directory name in each instance), or in several passes. Depth-first works better in the general case.

To convert filenames to lower case, if you have the utility mmv(1) on your machine, you could simply do:

# convert all filenames to lowercase
mmv "*" "#l1"

Otherwise, tr(1) may be helpful:

# tolower - convert file names to lower case
# POSIX
for file in "$@"
do
    [ -f "$file" ] || continue                # ignore non-existing names
    newname=$(echo "$file" | tr '[:upper:]' '[:lower:]')     # lower case
    [ "$file" = "$newname" ] && continue      # nothing to do
    [ -f "$newname" ] && continue             # don't overwrite existing files
    mv "$file" "$newname"
done

We use the fancy range notation, because tr can behave very strangely when using the A-Z range on some systems:

imadev:~$ echo Hello | tr A-Z a-z
hÉMMÓ

To make sure you aren't caught by surprise when using tr with ranges, either use the fancy range notations, or set your locale to C.

imadev:~$ echo Hello | LC_ALL=C tr A-Z a-z
hello
imadev:~$ echo Hello | tr '[:upper:]' '[:lower:]'
hello
# Either way is fine here.

This technique can also be used to replace all unwanted characters in a file name, e.g. with '_' (underscore). The script is the same as above, with only the "newname=..." line changed.

# renamefiles - rename files whose name contain unusual characters
# POSIX
for file in "$@"
do
    [ -f "$file" ] || continue            # ignore non-regular files, etc.
    newname=$(echo "$file" | sed 's/[^[:alnum:]_.]/_/g')
    [ "$file" = "$newname" ] && continue  # nothing to do
    [ -f "$newname" ] && continue         # do not overwrite existing files
    mv "$file" "$newname"
done

The character class in [] contains all the characters we want to keep (after the ^); modify it as needed. The [:alnum:] range stands for all the letters and digits of the current locale.

Here's an example that does the same thing, but this time using Parameter Expansion instead of sed:

# renamefiles (more efficient, less portable version)
# Bash
for file in "$@"; do
   [ -f "$file" ] || continue
   newname=${f//[^[:alnum:]_.]/_}
   [ "$file" = "$newname" ] && continue
   [ -f "$newname" ] && continue
   mv "$file" "$newname"
done

It should be noted that all three of these examples contain a [:RaceCondition:race condition] -- an existing file could be overwritten if it is created in between the [ -f "$newname" ... and mv "$file" ... commands. Solving this issue is beyond the scope of this page, however.

BashFAQ/030 (last edited 2020-06-10 14:10:22 by GreyCat)