4764
Comment: note GNU/BSD find requirement
|
8278
Use [[ rather than [ in the bash snippets, and printf rather than echo
|
Deletions are marked like this. | Additions are marked like this. |
Line 1: | Line 1: |
[[Anchor(faq30)]] | <<Anchor(faq30)>> |
Line 3: | Line 3: |
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}}}: |
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 non-recursive mass renames portably with a loop and some [[BashFAQ/073|Parameter Expansions]], like this: {{{ # POSIX # Rename all *.foo to *.bar for f in *.foo; do mv -- "$f" "${f%.foo}.bar"; done }}} {{{ # POSIX # This removes the extension .zip from all the files. for file in ./*.zip; do mv "$file" "${file%.zip}"; done }}} The "--" and "./*" are to protect from problematic filenames that begin with "-". You only need one or the other, not both, so pick your favorite. Here are some similar examples, using Bash-specific parameter expansions: {{{ # Bash # Replace all spaces with underscores for f in *\ *; do mv -- "$f" "${f// /_}"; done }}} {{{ # Bash # Rename all "foo" to "bar" for file in ./*foo*; do mv "$file" "${file//foo/bar}"; done }}} All the above examples invoke the external command {{{mv(1)}}} once for each file, so they may not be as efficient as some of the {{{rename}}} implementations. If you want to rename files recursively, then it becomes much more challenging. This example renames {{{*.foo}}} to {{{*.bar}}}: |
Line 27: | Line 44: |
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]. |
# 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 }}} This example uses Bash 4's `globstar` instead of GNU `find`: {{{ # Bash 4 which requires globstar to be enabled. NOT portable! # Rename all "foo" files to "bar" recursively. # "foo" must NOT appear in a directory name. shopt -s globstar for file in /path/to/**/*foo*; do mv -- "$file" "${file//foo/bar}" done }}} To check what will be the output of the above command, you can add an {{{echo}}} before the {{{mv}}} so you will get an idea. For more techniques on dealing with files with inconvenient characters in their names, see [[BashFAQ/020|FAQ #20]]. |
Line 41: | Line 75: |
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 file cd -- "$1" || exit for file in *; do [[ -d $file ]] && traverse "$file" ren "$file" done cd .. || exit } # 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=${f%/*} base=${f##*/}; mv "$f" "$dir/${base// /_}"; done' _ {} + }}} |
|
Line 49: | Line 120: |
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 }}} |
|
Line 54: | Line 132: |
for file in "$@" do |
for file in "$@"; do |
Line 57: | Line 134: |
newname=$(echo "$file" | tr '[:upper:]' '[:lower:]') # lower case | newname=$(printf %s "$file" | tr '[:upper:]' '[:lower:]') # lower case |
Line 60: | Line 137: |
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: |
mv -- "$file" "$newname" done }}} We use the fancy range notation, because {{{tr}}} can behave ''very'' strangely when using the {{{A-Z}}} range on some [[locale|locales]]: |
Line 86: | Line 163: |
for file in "$@" do |
for file in "$@"; do |
Line 89: | Line 165: |
newname=$(echo "$file" | sed 's/[^[:alnum:]_.]/_/g') | newname=$(printf '%s\n' "$file" | sed 's/[^[:alnum:]_.]/_/g') |
Line 92: | Line 168: |
mv "$file" "$newname" | mv -- "$file" "$newname" |
Line 104: | Line 180: |
[ -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. |
[[ -f $file ]] || continue newname=${file//[^[:alnum:]_.]/_} [[ $file = "$newname" ]] && continue [[ -f $newname ]] && continue mv -- "$file" "$newname" done }}} 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: when using GNU `mv`, on many file systems, attempting to rename a file to its lowercase or uppercase equivalent will fail. (This applies to Cygwin on DOS/Windows systems using FAT or NTFS file systems; to GNU `mv` on Mac OS X systems using HFS+ in case-insensitive mode; as well as to Linux systems which have mounted Windows/Mac file systems, and possibly many other setups.) GNU `mv` checks both the target names before attempting a rename, and due to the file system's mapping, it thinks that the destination "already exists": {{{ mv README Readme # fails with GNU mv on FAT file systems, etc. }}} 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 non-recursive mass renames portably with a loop and some Parameter Expansions, like this:
# POSIX # Rename all *.foo to *.bar for f in *.foo; do mv -- "$f" "${f%.foo}.bar"; done
# POSIX # This removes the extension .zip from all the files. for file in ./*.zip; do mv "$file" "${file%.zip}"; done
The "--" and "./*" are to protect from problematic filenames that begin with "-". You only need one or the other, not both, so pick your favorite.
Here are some similar examples, using Bash-specific parameter expansions:
# Bash # Replace all spaces with underscores for f in *\ *; do mv -- "$f" "${f// /_}"; done
# Bash # Rename all "foo" to "bar" for file in ./*foo*; do mv "$file" "${file//foo/bar}"; done
All the above examples invoke the external command mv(1) once for each file, so they may not be as efficient as some of the rename implementations.
If you want to rename files 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
This example uses Bash 4's globstar instead of GNU find:
# Bash 4 which requires globstar to be enabled. NOT portable! # Rename all "foo" files to "bar" recursively. # "foo" must NOT appear in a directory name. shopt -s globstar for file in /path/to/**/*foo*; do mv -- "$file" "${file//foo/bar}" done
To check what will be the output of the above command, you can add an echo before the mv so you will get an idea.
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 file cd -- "$1" || exit for file in *; do [[ -d $file ]] && traverse "$file" ren "$file" done cd .. || exit } # 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=${f%/*} 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=$(printf %s "$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=$(printf '%s\n' "$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=${file//[^[: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: when using GNU mv, on many file systems, attempting to rename a file to its lowercase or uppercase equivalent will fail. (This applies to Cygwin on DOS/Windows systems using FAT or NTFS file systems; to GNU mv on Mac OS X systems using HFS+ in case-insensitive mode; as well as to Linux systems which have mounted Windows/Mac file systems, and possibly many other setups.) GNU mv checks both the target names before attempting a rename, and due to the file system's mapping, it thinks that the destination "already exists":
mv README Readme # fails with GNU mv on FAT file systems, etc.
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