Differences between revisions 1 and 24 (spanning 23 versions)
Revision 1 as of 2007-05-02 23:18:24
Size: 3079
Editor: redondos
Comment:
Revision 24 as of 2010-12-28 20:44:43
Size: 7333
Editor: GreyCat
Comment: add bash 4 example for lowercasing. more comments.
Deletions are marked like this. Additions are marked like this.
Line 1: Line 1:
[[Anchor(faq30)]]
== How can I rename all my *.foo files to *.bar? How can I convert all upper-case file names to lower case? ==
<<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....
Line 4: Line 5:
Some GNU/Linux distributions have a rename 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.) Consult your system's man pages if you want to learn how to use your {{{rename}}} command, 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}}} examples here because it's too confusing -- there are two common versions of it and they're totally incompatible with each other.
Line 6: Line 7:
You can do mass renames in POSIX shells like this: You can do mass renames with [[BashFAQ/073|Parameter Expansion]], like this:
Line 9: Line 10:
for f in *.foo; do mv "$f" "${f%.foo}.bar"; done # POSIX
# Rename all *.foo to *.bar
for f in *.foo; do mv -- "$f" "${f%.foo}.bar"; done
Line 12: Line 15:
This invokes the external command {{{mv}}} 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 works (in ["BASH"]) as long as no files have newlines in their names:
The "--" is to protect from problematic filenames that begin with "-".
Here's a similar example, this time replacing spaces in filenames with underscores:
Line 17: Line 19:
find . -name '*.foo' -print | while IFS=$'\n' read -r f; do
  mv "$f" "${f%.foo}.bar"
# Bash
# Replace all spaces with underscores
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)
# Recursively change all *.foo files to *.bar

find . -type f -name '*.foo' -print0 | while IFS= read -r -d '' f; do
  mv -- "$f" "${f%.foo}.bar"
Line 22: Line 38:
To convert filenames to lower case: 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.

Here's an example script that uses depth-first recursion (changes spaces in names to underscores, but you just need to change the ren() function to do anything you want) to rename both files and directories (again, it's easy to modify to make it act only on files or only on directories, or to act only on files with a certain extension, to avoid or force overwriting files, etc.):

{{{
# Bash
ren() {
  local newname
  newname=${1// /_}
  [ "$1" != "$newname" ] && mv -- "$1" "$newname"
}

traverse() {
  local i
  cd -- "$1" || exit 1
  for i in *; do
    [ -d "$i" ] && traverse "$i"
    ren "$i"
  done
  cd .. || exit 1
}

# main program
shopt -s nullglob
traverse /path/to/startdir
}}}

Here is another way to recursively rename all directories and files with spaces in their names:

{{{
find . -depth -name "* *" -exec bash -c 'dir=${1%/*} base=${1##*/}; mv "$1" "$dir/${base// /_}"' _ {} \;
}}}

or, if your version of `find` accepts it, this is more efficient as it runs one bash for many files instead of one bash per file:

{{{
find . -depth -name "* *" -exec bash -c 'for f; do dir=${1%/*} base=${f##*/}; mv "$f" "$dir/${base// /_}"; done' _ {} +
}}}

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, you need something that can take a mixed-case filename as input and give back the lowercase version as output. In Bash 4 and higher, there is a [[BashFAQ/073|parameter expansion]] that can do it:

{{{
# Bash 4
for f in *[[:upper:]]*; do mv -- "$f" "${f,,*}"; done
}}}

Otherwise, {{{tr(1)}}} may be helpful:
Line 26: Line 103:

for file in *
# POSIX
for file in "$@"
Line 29: Line 106:
    [ -f "$file" ] || continue   # ignore non-existing names
    newname=$(echo "$file" | tr 'A-Z' 'a-z') # lower-case version of file name
    [ "$file" = "$newname" ] && continue   # nothing to do
    [ -f "$newname" ] && continue   # do not overwrite existing files
    mv "$file" "$newname"
    [ -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"
Line 37: Line 114:
Purists will insist on using
{{{
tr '[:upp
er:]' '[:lower:]'
}}}
in
the above code, in case of non-ASCII (e.g. accented) letters in locales which have them. Note that {{{tr}}} can behave ''very'' strangely when using the {{{A-Z}}} range on some systems:
We use the fancy range notation, because {{{tr}}} can behave ''very'' strangely when using the {{{A-Z}}} range on some [[locale|locales]]:
Line 45: Line 118:
hÃMMÃ hÉMMÓ
Line 48: Line 121:
To make sure you aren't caught by surprise when using {{{tr}}}, either use the fancy range notations, or set your locale to C. 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.
Line 58: Line 131:
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 62: Line 135:
for file in * # POSIX
for file in "$@"
Line 64: Line 138:
    [ -f "$file" ] || continue       # ignore non-existing names
    newname=$(echo "$file" | sed 's/[^a-zA-Z0-9_.]/_/g')
    [ "$file" = "$newname" ] && continue       # nothing to do
    [ -f "$newname" ] && continue       # do not overwrite existing files
    mv "$file" "$newname"
    [ -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"
Line 72: Line 146:
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 74: Line 148:
If you have the utility "mmv" on your machine, you could simply do Here's an example that does the same thing, but this time using Parameter Expansion instead of {{{sed}}}:
Line 77: Line 151:
mmv "*" "#l1" # 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
Line 80: Line 162:
Another common form of this question is "How do I rename all my MP3 files so that they have underscores instead of spaces?" You can use this: It should be noted that all 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.

One final note about changing the case of filenames: on many DOS/Windows-based file systems (FAT/FAT32 at least), it is not possible to rename a file to its lowercase or uppercase equivalent. (This applies to Cygwin, as well as to Linux systems which have mounted Windows file systems, and possibly many other setups.) Files on FAT file systems retain the case with which they were originally created, but any reference to the same name with a different case is mapped back to the already-existing file. Thus, for example:
Line 83: Line 167:
for f in *\ *.mp3; do mv "$f" "${f// /_}"; done mv README Readme # fails on FAT file systems
Line 85: Line 169:

The workaround for this is to rename the file twice: first to a temporary name which is completely different from the original name, then to the desired name.

{{{
mv README tempfilename &&
mv tempfilename Readme
}}}

----
CategoryShell

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 your rename command, 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 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 Parameter Expansion, like this:

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

The "--" is to protect from problematic filenames that begin with "-". Here's a similar example, this time replacing spaces in filenames with underscores:

# Bash
# Replace all spaces with underscores
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)
# Recursively change all *.foo files to *.bar

find . -type f -name '*.foo' -print0 | while IFS= read -r -d '' f; do
  mv -- "$f" "${f%.foo}.bar"
done

For more techniques on dealing with files with inconvenient characters in their names, see 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.

Here's an example script that uses depth-first recursion (changes spaces in names to underscores, but you just need to change the ren() function to do anything you want) to rename both files and directories (again, it's easy to modify to make it act only on files or only on directories, or to act only on files with a certain extension, to avoid or force overwriting files, etc.):

# Bash
ren() {
  local newname
  newname=${1// /_}
  [ "$1" != "$newname" ] && mv -- "$1" "$newname"
}

traverse() {
  local i
  cd -- "$1" || exit 1
  for i in *; do
    [ -d "$i" ] && traverse "$i"
    ren "$i"
  done
  cd .. || exit 1
}

# main program
shopt -s nullglob
traverse /path/to/startdir

Here is another way to recursively rename all directories and files with spaces in their names:

find . -depth -name "* *" -exec bash -c 'dir=${1%/*} base=${1##*/}; mv "$1" "$dir/${base// /_}"' _ {} \;

or, if your version of find accepts it, this is more efficient as it runs one bash for many files instead of one bash per file:

find . -depth -name "* *" -exec bash -c 'for f; do dir=${1%/*} base=${f##*/}; mv "$f" "$dir/${base// /_}"; done' _ {} +

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, you need something that can take a mixed-case filename as input and give back the lowercase version as output. In Bash 4 and higher, there is a parameter expansion that can do it:

# Bash 4
for f in *[[:upper:]]*; do mv -- "$f" "${f,,*}"; done

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 locales:

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 these examples contain a 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.

One final note about changing the case of filenames: on many DOS/Windows-based file systems (FAT/FAT32 at least), it is not possible to rename a file to its lowercase or uppercase equivalent. (This applies to Cygwin, as well as to Linux systems which have mounted Windows file systems, and possibly many other setups.) Files on FAT file systems retain the case with which they were originally created, but any reference to the same name with a different case is mapped back to the already-existing file. Thus, for example:

mv README Readme    # fails on FAT file systems

The workaround for this is to rename the file twice: first to a temporary name which is completely different from the original name, then to the desired name.

mv README tempfilename &&
mv tempfilename Readme


CategoryShell

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