Skip to content
Shell & TerminalDeep Dive Published Updated 8 min readViews unavailable

mv in Production: Rename Atomicity, Cross-Filesystem Copies, and Safe Publication

Use mv with explicit destination, replacement, and filesystem assumptions, and distinguish atomic rename from copy-then-remove fallback behavior.

mv is often treated as a universal atomic file-publication primitive. It is not. A same-filesystem rename is usually the useful namespace operation people have in mind, but a move across filesystems cannot be implemented by one ordinary rename. GNU mv falls back to copying the source and, after a successful copy, removing the original. Directory trees may therefore be only partly moved after an error. Replacement rules, symlinks, trailing slashes, concurrent writers, and durability requirements introduce additional contracts that a deployment script should state explicitly.

The word “atomic” is also overloaded. A rename can make a name switch appear indivisible to concurrent pathname lookups on a particular filesystem, but it does not validate the bytes, coordinate multiple writers, guarantee power-loss durability, or make a cross-filesystem transfer transactional. Treat publication as a small protocol: prepare data, validate it, publish the name, and decide separately how to handle recovery and persistence.

Rename versus move

When source and destination are on the same filesystem, mv generally delegates to a rename operation. The inode is not copied byte by byte; the directory entry is changed. Existing open descriptors continue to refer to the already-open object, while later pathname lookups observe the namespace after the operation. When publishers stage completed immutable files and the filesystem provides the expected rename semantics, readers opening the destination before or after replacement see the old or new name binding rather than a half-copied file. This does not prevent another process with an open writable descriptor from changing the underlying file contents.

That description does not cover every command-line shape. If the destination names an existing directory, mv source destination may place the source inside it rather than replace the directory entry the author intended. A script should make whether the operand is a directory or a single destination unambiguous. GNU’s -T forces the latter interpretation; it is an implementation extension, so portable scripts should use a carefully controlled destination path and test on their target systems. Avoid ambiguous multi-source forms when the deployment invariant is “replace exactly this pathname.”

Across filesystems, rename normally fails with a cross-device error. GNU mv then copies attributes and content and removes the original after success. The destination can become visible before the copy completes. GNU documents that when it detects a copy failure, it removes the partial destination copy; however, abrupt termination can bypass cleanup, and a completed destination can coexist temporarily with its source before removal. For directory batches, successful earlier entries are not rolled back if a later entry fails. If cross-filesystem behavior must be rejected rather than silently changed, GNU mv --no-copy is explicit; otherwise, stage a new copy on the destination filesystem and publish there only after validation.

Build a same-filesystem publication protocol

Create the temporary artifact inside the destination directory, not in a generic temporary directory that may reside on another filesystem. The following GNU-oriented sketch copies into a private temporary file, checks the copy, and then renames it over the destination:

set -eu
source_file=$1
destination=$2
directory=${destination%/*}
[ "$directory" = "$destination" ] && directory=.
temporary=$(mktemp "$directory/.publish.XXXXXX")
cleanup() {
    status=$?
    trap - EXIT
    rm -f -- "$temporary" || printf '%s\n' 'warning: could not remove temporary artifact' >&2
    exit "$status"
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM

cp -- "$source_file" "$temporary"
validate-artifact "$temporary"
chmod 0644 "$temporary"
mv -f -- "$temporary" "$destination"
trap - EXIT HUP INT TERM

This sketch assumes the application supplies validate-artifact, that the directory is trusted, and that the temporary file and destination are on the same filesystem. mktemp templates and cp -- are not identical across all Unix systems; use the platform’s documented secure temporary-file API when portability matters. A production publisher should also preserve required ownership, ACLs, extended attributes, labels, and mode bits intentionally rather than inheriting them by accident. If a hostile user can modify the destination directory, shell-level pathname checks do not close symlink and replacement races; use a privileged deployment service or descriptor-based API designed for that trust boundary.

The rename step protects readers from partial file contents, but not from two publishers racing. If writer A validates release 10, writer B publishes release 11, and A then renames, the older release can win last. Serialize publishers with a lock or compare the intended version immediately before commit. For multi-file deployments, a single file rename cannot atomically publish an entire tree. A common design is to create a versioned release directory and atomically switch one symlink or manifest pointer after the directory is complete.

Replacement policy is part of correctness

mv -f expresses replacement intent but does not create a compare-and-swap operation. mv -n can avoid replacing an existing destination in implementations that support it, but its behavior and status details need platform-specific review. Interactive prompts are not a safe automation policy: a noninteractive job can hang, receive unexpected input, or behave differently when standard input is redirected. State whether a destination must be absent, may be replaced, or must match a previously observed version.

If the target is a symlink, ask whether the operation should replace the link entry or operate on its referent. A trailing slash on a symlink-to-directory operand has historically triggered surprising implementation-specific behavior. GNU’s manual advises avoiding source names with a trailing slash when they may be symlinks to directories. Normalize and validate path operands before destructive actions, but remember that lexical normalization does not resolve races or establish object identity.

Verify outcomes and failure paths

Test more than the happy path. Exercise same-filesystem moves, cross-filesystem moves, an existing destination, a read-only directory, insufficient space, interrupted copies, symlink operands, and paths with spaces or leading hyphens. Check the command’s exit status and inspect both source and destination after failure; do not infer that a nonzero status means nothing changed. For release publication, retain the prior version until the new release passes health checks, and make rollback a separate controlled rename or pointer switch.

Finally, rename success is not the same as durable commit after sudden power loss. The shell utility does not by itself express the full file-and-directory synchronization sequence required by a particular storage stack. Databases and package managers should use their documented durability APIs. For ordinary deployments, record the filesystem assumptions and recovery procedure instead of advertising an unqualified “atomic move.”

Failure matrix for release tooling

Write down observable states for each failure point. Before staging begins, the old destination should remain usable. During a copy into a temporary sibling, readers should continue to use the old version. Validation failure should remove only the temporary file. Rename failure should leave the old name intact where the filesystem contract guarantees that property, while the diagnostic should preserve the reason. After rename, a health-check failure is not automatically a reason to delete the new release; rollback should restore a known-good pointer and retain the failed artifact for diagnosis.

For cross-device moves, the state table is different: the source may still exist while the destination is partial, the destination may be complete while source removal failed, or a directory tree may be partly transferred. Recovery must be idempotent. A retry should inspect content identity and destination state instead of blindly overwriting whichever version is present. Use checksums or manifests to establish whether a staged artifact is complete; timestamps and file sizes alone do not prove byte equality.

Multi-file deployments need an explicit commit point. A robust pattern builds an immutable tree under a versioned name, verifies every required file, writes a manifest, and then updates one pointer such as a symlink or a small manifest file. Readers should resolve that pointer once per request rather than mix files from two releases. Keep old trees long enough for open processes and rollback, and make cleanup aware of active readers, retention policy, and backups. These rules turn a collection of mv commands into a comprehensible release protocol.

Metadata and interoperability checks

Copying between filesystems can change ownership, mode interpretation, ACLs, security labels, timestamps, sparse layout, and extended attributes. GNU mv attempts to preserve some attributes while copying, but platform and filesystem behavior differ. A move from a case-sensitive filesystem to a case-insensitive one can also collide names that were previously distinct. A directory tree with names that differ only by normalization or case should be tested on the destination volume before publication.

Run a deployment rehearsal on the same filesystem type, mount options, credentials, and tooling as production. Include an existing destination, a symlink destination, a large sparse file, a file with ACLs, and a forced low-space failure. Verify both content and metadata after completion. If the environment cannot reproduce the production storage stack, describe that gap as an untested assumption rather than treating local success as proof. The purpose of the rehearsal is to verify the whole protocol, not merely that mv exits zero once.

Related:

Sources:

Comments