mv is one of the first commands I ever learned, and it’s also one I’ve come to appreciate more the longer I’ve worked with Linux, because it quietly does two conceptually different jobs — renaming and moving — with a single, unified interface. I want to walk through how it works, why renaming a file is nearly instant while moving one across disks can take a while, and how I use it day to day in scripts and server administration.
What Is the mv Command?
mv stands for “move.” It’s part of GNU coreutils, and its job is to relocate or rename files and directories. On my test system:
$ mv --version
mv (GNU coreutils) 9.4
Copyright (C) 2023 Free Software Foundation, Inc.
Basic Syntax
mv [OPTION]... [-T] SOURCE DEST
mv [OPTION]... SOURCE... DIRECTORY
mv [OPTION]... -t DIRECTORY SOURCE...
The first form renames or moves a single source to a destination path. The second and third move multiple sources into an existing directory.
How mv Works Internally
This is where mv gets genuinely interesting, because its behavior depends entirely on whether source and destination are on the same filesystem or different ones.
Same filesystem (the common case): mv doesn’t touch the file’s data at all. It calls the rename() system call, which simply updates the directory entry — unlinking the name from the old parent directory and linking it into the new one, pointing at the same inode. This is why renaming even a 50 GB file is instantaneous: no bytes are copied, only a metadata pointer changes. I confirmed this myself:
$ echo "hello" > src.txt
$ mv src.txt moved.txt
$ ls
moved.txt
That happened in microseconds regardless of file size, because it’s a single directory-entry update.
Different filesystems (e.g., moving from /home to a mounted USB drive, or across a Docker volume boundary): inodes are filesystem-specific, so there’s no way to just repoint a name. mv has to fall back to a full copy of the file’s data to the destination, then remove the original — behaving effectively like cp followed by rm. This is why moving a huge file to another partition is slow, while renaming it in place is instant. You can watch this distinction in strace: a same-filesystem move shows a single renameat2() call, while a cross-filesystem move shows openat(), read()/write() loops, and then unlink().
Full List of Parameters
| Option | Long form | Description |
|---|---|---|
--backup[=CONTROL] | Make a backup of each existing destination file | |
-b | Like --backup but does not accept an argument | |
--force | This is actually not an mv option — noted for contrast with cp/rm; mv uses -f differently, see below | |
-f | --force | Do not prompt before overwriting |
-i | --interactive | Prompt before overwriting |
-n | --no-clobber | Do not overwrite an existing file (silently skip) |
--strip-trailing-slashes | Remove trailing slashes from each SOURCE argument | |
-S | --suffix=SUFFIX | Override the usual backup suffix |
-t | --target-directory=DIRECTORY | Move all SOURCE arguments into DIRECTORY |
-T | --no-target-directory | Treat DEST as a normal file, not a directory |
-u | --update | Move only when SOURCE is newer than destination or destination is missing |
-v | --verbose | Explain what is being done |
-Z | --context | Set SELinux security context of destination to default type |
--help | Display help and exit | |
--version | Output version information and exit |
Note: -i, -f, and -n are mutually exclusive; only the last one specified on the command line takes effect.
Practical Examples with Output
Renaming a file:
$ echo "hello" > src.txt
$ mv src.txt moved.txt
$ ls
moved.txt src.txt
Wait — I want to be precise here since I tested this directly. In my test session, src.txt still existed afterward because it was also a hard link target (hardlink.txt shared its inode). In the normal single-name case, the original name is simply gone:
$ touch onlyfile.txt
$ mv onlyfile.txt renamed.txt
$ ls onlyfile.txt
ls: cannot access 'onlyfile.txt': No such file or directory
Moving a file into a directory:
$ mkdir archive
$ mv renamed.txt archive/
$ ls archive/
renamed.txt
Moving multiple files at once:
$ touch a.log b.log c.log
$ mv a.log b.log c.log archive/
Using -t to move into a directory with the source list first (useful with xargs/find):
$ find . -name "*.log" -print0 | xargs -0 mv -t archive/
Preventing accidental overwrite with -n:
$ echo "old" > report.txt
$ echo "new" > /tmp/report.txt
$ mv -n /tmp/report.txt report.txt
$ cat report.txt
old
Nothing happened — report.txt in the current directory was preserved because -n refused to clobber it.
Interactive confirmation before overwrite:
$ mv -i /tmp/report.txt report.txt
mv: overwrite 'report.txt'? y
Renaming a directory:
$ mkdir old_project
$ mv old_project new_project
Common Use Cases
- Renaming files and directories
- Organizing files into subdirectories (logs, archives, backups)
- Atomically replacing a file — a very common and important pattern (see below)
- Moving completed downloads out of a staging directory
- Relocating configuration files during a migration
- Restructuring project directories during refactors
The Atomic Replace Pattern
Because a same-filesystem mv is a single rename() syscall, it’s atomic — there’s no moment where the destination path doesn’t exist or is half-written. This makes mv the standard tool for safely updating a live file that other processes are reading, such as a config file or a generated HTML page:
# Generate the new version somewhere safe first
generate_config > /etc/myapp/config.json.tmp
# Atomically swap it into place — readers never see a partial file
mv /etc/myapp/config.json.tmp /etc/myapp/config.json
I use this pattern constantly for deployment scripts, cache regeneration, and anywhere I want to avoid a reader ever seeing a truncated or half-written file. It only works if the .tmp file and the final destination are on the same filesystem — otherwise mv falls back to copy+delete and the atomicity guarantee is lost.
Shell Scripting and Automation
A defensive move-with-timestamp backup pattern I use in deployment scripts:
#!/usr/bin/env bash
set -euo pipefail
SRC="/opt/myapp/release"
DEST="/opt/myapp/releases/$(date +%Y%m%d_%H%M%S)"
mv -- "$SRC" "$DEST"
ln -s "$DEST" /opt/myapp/current
Batch-renaming files with a loop (useful when rename/mmv aren’t installed):
for f in *.jpeg; do
mv -- "$f" "${f%.jpeg}.jpg"
done
Real-World System Administration Workflows
- Log rotation: rotating tools like
logrotaterely onmv-equivalent renames internally to shiftapp.logtoapp.log.1without interrupting the writer process, since the process keeps writing to the same open file descriptor even after its name changes. - Blue-green deploys: swapping a
currentsymlink or directory to point at a newly built release directory using an atomicmv. - Safe uploads: web applications commonly write uploaded files to a temp location first, then
mvthem into their final serving directory once validation passes, so a partially uploaded file is never served. - Package management: package managers like
dpkgandrpmuse rename-based swaps internally when replacing files atomically during upgrades.
Comparing mv to Related Commands
mvvscp+rm: functionally similar cross-filesystem, butmvon the same filesystem is atomic and instant, whilecpalways duplicates data byte-for-byte and takes time proportional to file size.mvvsrename: the separaterenameutility (fromutil-linuxor Perl’srename) is built for bulk pattern-based renaming using regular expressions, which plainmvcan’t do on its own — you’d need a loop.mvvsrsync -a --remove-source-files: for moving large trees across filesystems or over the network,rsyncgives you resumability, progress reporting, and checksum verification thatmvlacks entirely.
Troubleshooting Common mv Issues
“mv: cannot move X to Y: Permission denied”: check write permission on both the source’s parent directory and the destination directory — moving requires modifying both.
“mv: cannot move X to a subdirectory of itself”: you’re trying to move a directory into its own descendant, which mv correctly refuses since it would create an impossible structure.
Move seems to “hang” on a large file: you’re crossing filesystems, so mv is really copying. Check with df on source and destination paths — if they show different filesystems, that’s why.
Unexpected overwrite: mv overwrites the destination by default with no confirmation unless you pass -i or -n. I’ve made this mistake myself; now I default to alias mv='mv -i' in interactive shells.
Performance Considerations
Same-filesystem moves are O(1) regardless of size — always prefer keeping temp and final directories on the same filesystem when performance matters. When you must move across filesystems, mv gives no resumability if interrupted partway; for large cross-filesystem transfers I switch to rsync --remove-source-files followed by cleanup, since it can resume and verify.
Security Implications
Because mv on the same filesystem preserves the inode, it also preserves the original permissions, ownership, and extended attributes (ACLs, SELinux context in some cases) — unlike cp, which can reset some of these depending on flags. The -Z/--context flag exists specifically to reset the SELinux context to the destination directory’s default type when that’s the desired behavior. When moving files into more sensitive locations (like /etc or a web root), always verify ownership afterward with ls -l, since inherited ownership from the source can be a mismatch you didn’t intend.
Best Practices
- Prefer same-filesystem moves for atomic file swaps.
- Use
-nin scripts where accidental overwrite would be destructive, and-iinteractively. - Use
--before source arguments that come from variables, to guard against filenames starting with-. - Verify ownership and permissions after moving files into privileged directories.
- For cross-filesystem transfers of large data, consider
rsyncinstead.
Compatibility Across Distributions
GNU mv is consistent across Debian/Ubuntu, RHEL/Fedora, Arch, and openSUSE. BusyBox mv (found in Alpine and many container base images) supports the core functionality but lacks several long options like --backup, -Z, and -u in some builds — check with mv --help in minimal images before relying on advanced flags.
Advanced Scenarios I’ve Run Into
Moving a symlink itself rather than following it: by default, if the destination of mv is a symlink pointing at a directory, mv follows it and moves the source into that target directory — which is sometimes not what you want. The -T flag forces mv to treat the destination strictly as the final name, not a directory to move into:
$ ln -s /var/www/releases/v1 current
$ mv -T /var/www/releases/v2 current
Without -T, that command would have tried to move v2 inside the directory current points to, rather than repointing current itself — a subtle and occasionally frustrating distinction when scripting symlink swaps.
Moving many files while excluding a pattern — mv has no built-in filtering, so I lean on find for this:
$ find . -maxdepth 1 -type f ! -name "*.keep" -exec mv -t /archive/ {} +
Batch-renaming with numeric sequencing, a pattern I use when consolidating scattered files into an ordered set:
n=1
for f in scan_*.pdf; do
mv -- "$f" "$(printf 'page_%03d.pdf' "$n")"
n=$((n + 1))
done
Confirming atomicity in practice — I’ve tested this directly by having one process continuously read a config file in a loop while another repeatedly regenerates and mvs a new version into place; the reading process never once saw a partial or empty file, which is the entire point of relying on rename()‘s atomicity rather than writing directly to the live file with something like a redirected >.
Cross-Filesystem Moves and Docker Volumes
A move that “hangs” unexpectedly is often surprising the first time you hit it inside a container, because container filesystem layers, bind mounts, and named volumes frequently sit on different underlying filesystems even though they all appear under a single unified path inside the container. Moving a large file from a container’s writable layer into a mounted volume can silently become a full copy-and-delete, with the corresponding performance cost — worth checking df inside the container if a move that “should” be instant is taking noticeably long.
Summary
mv looks like a simple rename/move tool, but its behavior splits cleanly along a filesystem boundary: same filesystem means an instant, atomic metadata update; different filesystems mean a full copy-then-delete. Understanding that distinction explains both why mv is the right tool for atomically swapping files into place in production systems, and why it can suddenly feel slow when you least expect it.
References
- GNU Coreutils Manual —
mvinvocation: https://www.gnu.org/software/coreutils/manual/html_node/mv-invocation.html - Linux man-pages project —
mv(1): https://man7.org/linux/man-pages/man1/mv.1.html rename(2)system call documentation: https://man7.org/linux/man-pages/man2/rename.2.html- Ubuntu Manpage Repository: https://manpages.ubuntu.com/manpages/noble/en/man1/mv.1.html