The Fundamental Pattern: A for-loop With mv
Almost every bash rename is a variation on one idea: loop over the files, and for each one, move it from its old name to a new name. The tool that does the moving is mv, and in the shell a rename is just a move to a different name in the same folder.
The canonical form looks like this. Run for f in *.txt; do mv "$f" "${f%.txt}.bak"; done. Read it left to right: for f in *.txt walks over every file whose name ends in .txt, binding each one to the variable f in turn. Inside the loop, mv "$f" "${f%.txt}.bak" moves that file to a new name. The interesting piece is ${f%.txt}, which is parameter expansion that strips the .txt from the end of the name, leaving the base; appending .bak gives you the new extension. So report.txt becomes report.bak, and every other .txt file in the folder follows the same rule.
That single line is the workhorse. Change the glob (*.txt) to select different files, and change the expansion inside the mv to describe the transform, and you can express most renames you will ever need. Everything below is really just a menu of transforms you can drop into the same loop.
Bash parameter expansion is the built-in string surgery that makes the loop useful. It runs inside the shell with no external program, and once you know the four patterns below you can build most renames from memory.
To strip an extension, use ${f%.txt}. The percent sign removes the shortest match of .txt from the end of the value, so photo.txt becomes photo. Use a double percent, ${f%%.tar.gz}, to remove the longest match from the end when a name has more than one dot. The mirror image is the hash: ${f#prefix_} removes a matching prefix from the front, and ${f##*/} removes everything up to the last slash, which is how you strip a leading path.
To replace a substring anywhere in the name, use ${f//old/new}. A single slash, ${f/old/new}, replaces only the first occurrence; the double slash replaces every occurrence. This is how you swap one word for another across a batch: for f in *; do mv "$f" "${f// /_}"; done turns every space in every name into an underscore, which is the single most common cleanup people reach for.
To add a prefix or a suffix, you concatenate. For a prefix, mv "$f" "2026_$f" puts 2026_ in front of each name. For a suffix that sits before the extension, combine a strip with an append: mv "$f" "${f%.jpg}_edited.jpg" turns beach.jpg into beach_edited.jpg. Between stripping, replacing, and concatenating, parameter expansion covers the large majority of real renaming work without ever leaving bash.
The rename Command: Regex Renames
When the transform gets more complex than a simple substring swap, the rename command is often cleaner than a loop. It applies a pattern to a list of files in one call, and the most capable version speaks regular expressions.
There are two different programs both called rename, and this trips people up constantly. The Perl version (sometimes packaged as perl-rename or prename, and the default on many Debian and Ubuntu systems) takes a Perl expression as its first argument. To replace every space with an underscore across a folder, run rename 's/ /_/g' *. The s/ /_/g is a standard Perl substitution: swap a space for an underscore, globally, on each file name. The other version ships with util-linux and uses a simpler from-to-files syntax: rename .txt .bak *.txt replaces the literal string .txt with .bak. The two are not interchangeable, so check which one you have before copying a command; running rename --version usually tells you.
Whichever variant you have, the most important flag is the dry run. On the Perl rename, rename -n 's/ /_/g' * prints exactly what each file would be renamed to without touching anything. Run that first, read the output, and only drop the -n once you are satisfied the pattern selects the files you meant and produces the names you expected.
Case Conversion With tr
Lowercasing or uppercasing a batch of names is a job for tr, which translates one set of characters into another. Inside a loop, pipe the name through it: for f in *; do mv "$f" "$(echo "$f" | tr '[:upper:]' '[:lower:]')"; done lowercases every file name in the folder. Swap the two character classes to go the other way and force uppercase.
Modern bash (version 4 and later) can also do case conversion in pure parameter expansion, without tr at all: ${f,,} lowercases the whole value and ${f^^} uppercases it. So mv "$f" "${f,,}" is a shorter way to lowercase a name if your bash is recent enough. The tr approach is worth knowing because it works everywhere, including older systems and other shells, while the ${f,,} form is tidier when you know you have bash 4 plus.
Dry-Run Safety: The Habit That Saves Your Folder
Bash does not have an undo. mv overwrites silently, so if two source names collapse to the same target, the first file is simply gone. There is no recycle bin and no confirmation. This is the single most important thing to internalise about renaming in the shell, so it gets its own section.
The fix is a habit, not a tool: preview every batch before you run it for real. The simplest preview is to put echo in front of the mv. Run for f in *.txt; do echo mv "$f" "${f%.txt}.bak"; done and bash prints each mv command it would run instead of executing it. Read the output. Confirm the glob matched the files you expected, confirm the new names look right, and confirm no two lines produce the same target. Only then remove the echo and run it for real. For the rename command, the equivalent is the -n flag described above.
Two more rules keep the loop from surprising you. First, quote every variable: write "$f", not $f. An unquoted variable splits on spaces, so a file named Vacation Photo 1.JPG becomes three separate arguments to mv and the command falls apart. The quotes keep the whole name together as one thing. Second, be specific with your glob. *.txt is safer than *, because a bare star sweeps in directories, hidden files, and anything else in the folder. Narrow the pattern to exactly the files you mean to touch, and you remove a whole class of accidents before they can happen.
A concrete before and after makes the payoff clear. A phone dump full of names like Vacation Photo 1.JPG, with a capitalised extension and spaces that break half your scripts, becomes vacation_photo_1.jpg: lowercased, spaces replaced with underscores, and the extension normalised. One loop, applied to the whole folder, and the batch is consistent.
The three approaches overlap, but each has a job it does best. Use this to pick quickly rather than reaching for the same one out of habit.
Bash renaming approaches compared
| Approach | Best for | Regex support | Dry run |
|---|
| for-loop with mv | Any transform you can express with parameter expansion; full control and no dependencies | No (uses parameter expansion instead) | Prefix mv with echo to preview |
| rename (Perl) | Regex renames across a whole folder in one call | Yes (Perl substitutions like s/ /_/g) | Built in via the -n flag |
| mmv | Wildcard-to-wildcard mass moves with a from-pattern and a to-pattern | No (glob-style wildcards, not regex) | Use the -n flag to preview |
In practice, most people live in the first two rows. The for-loop is the default because it is always available and endlessly flexible; reach for the Perl rename when the transform is genuinely a regular expression and a loop would be awkward. mmv is a separate install and worth it only if wildcard-to-wildcard moves are a regular part of your work.
One Thing Bash Cannot Do: Rename by Content
Every technique on this page renames by pattern. Bash sees a file name as a string and transforms that string; it never looks inside the file. That is exactly what you want when the information you need is already in the name, but it hits a hard wall the moment the useful information lives inside the document instead.
A folder of scanned PDFs named scan0001.pdf, scan0002.pdf, and so on is the classic case. No loop, no regex, and no parameter expansion can turn scan0001.pdf into 2026-03-14_acme_invoice_4471.pdf, because bash cannot read the vendor, the date, or the invoice number off the scanned page. That data is pixels inside the file, not text in the name.
Where a Content-Aware Tool Fits
For the pattern-based renames above, bash is the right tool and this page is your reference. For renaming by what is inside the file, you need something that reads the document. Renamer.ai is a GUI content-renamer for Windows and Mac that uses OCR to read a scanned or digital document and name it from its contents; to be honest about it, it is not a shell tool and does not replace bash for pattern work. If your batch is scans and the useful information is trapped inside them, that is the gap it fills; for everything else on this page, the shell has you covered. For the wider set of bulk renaming tools beyond the command line, see bulk rename software.