A Portable Bash Function for Safe File Moving
Why I Keep a Portable Bash Function for Safe File Moving
In my daily work I often need to move large sets of log files from one directory to another while preserving permissions and avoiding accidental overwrites. The built‑in mv command is fast, but it can silently clobber existing files unless you add extra checks. Over time I’ve wrapped this pattern into a reusable function that gives me a safety net without sacrificing speed.
Real‑world scenario
Imagine a nightly batch job that aggregates application logs into a staging directory before they are shipped to a central analytics pipeline. The staging area is shared among several services, so two jobs might try to place a file with the same name at the same time. If the second job runs mv without checking, it will overwrite the first job’s output, causing data loss. The function below solves this by:
- Checking whether the destination already exists.
- If it does, appending a timestamp or a numeric suffix to make the name unique.
- Moving the file with
mv -i(interactive) as a fallback, but the script never prompts because we handle the naming ourselves. - Preserving original attributes with
mv --preserve=mode,ownership,timestampson systems that support it (GNU coreutils).
The function
# Move a file safely, avoiding overwrites by generating a unique name if needed.
# Usage: safe_move SOURCE DEST_DIR
safe_move() {
local src="$1" dest_dir="$2"
# Basic validation
[[ -z "$src" || -z "$dest_dir" ]] && { echo "Usage: safe_move SOURCE DEST_DIR" >&2; return 1; }
[[ ! -e "$src" ]] && { echo "Error: source '$src' does not exist" >&2; return 2; }
[[ ! -d "$dest_dir" ]] && { echo "Error: destination '$dest_dir' is not a directory" >&2; return 3; }
# Extract the base name (filename) from the source path
local base_name
base_name="$(basename -- "$src")"
# Build the initial destination path
local dest_path="$dest_dir/$base_name"
# If the destination already exists, create a unique variant
if [[ -e "$dest_path" ]]; then
# Try appending a timestamp first
local timestamp
timestamp="$(date +%Y%m%d%H%M%S)"
local candidate="$dest_dir/${base_name%.*}_$timestamp.${base_name##*.}"
# If the file has no extension, adjust accordingly
if [[ "$base_name" != *.* ]]; then
candidate="$dest_dir/${base_name}_$timestamp"
fi
# If the timestamped name also exists (unlikely but possible), fall back to a numeric loop
if [[ -e "$candidate" ]]; then
local i=1
while [[ -e "$dest_dir/${base_name%.*}_$i.${base_name##*.}" ]]; do
((i++))
done
candidate="$dest_dir/${base_name%.*}_$i.${base_name##*.}"
if [[ "$base_name" != *.* ]]; then
candidate="$dest_dir/${base_name}_$i"
fi
fi
dest_path="$candidate"
echo "Warning: '$base_name' already exists in '$dest_dir'. Renaming to '$(basename -- "$dest_path")'" >&2
fi
# Perform the move, preserving metadata where possible
if mv --preserve=mode,ownership,timestamps -- "$src" "$dest_path" 2>/dev/null; then
return 0
else
# Fallback for systems that don't support --preserve
mv -- "$src" "$dest_path"
fi
}
Why this works
The function separates concerns: validation, name generation, and the actual move. By checking for existence up front we avoid the race condition that a simple mv -n would still have if another process creates the file after the check but before the move. In practice the window is tiny, and for most batch jobs it’s acceptable. If you need stronger guarantees you could rename the source to a temporary unique name first, then move it, but that adds complexity that rarely pays off.
Using date +%Y%m%d%H%M%S gives a human‑readable timestamp that sorts lexicographically, making it easy to later identify when a conflict occurred. The numeric fallback guarantees termination even if the clock were to go backwards (a scenario that can happen in containers with faulty NTP).
Preserving ownership and timestamps is important when the moved files are later consumed by processes that rely on those attributes (e.g., backup tools that increment based on mtime). The GNU mv flag --preserve does exactly that; the fallback ensures the script still runs on macOS or BSD systems where the flag is unavailable.
Edge cases and extensions
While the function handles regular files well, there are a few situations you might want to adapt:
- Directories: The same logic works if you pass a directory as
SOURCE. The timestamp is appended to the directory name, preserving its contents. - Multiple sources: If you need to move many files at once, wrap the call in a
forloop. The function will generate a unique name for each source individually, preventing clashes between them. - Custom naming schemes: Replace the timestamp with a UUID (
uuidgen) or a hash of the file contents if you need stronger uniqueness guarantees.
How to integrate it
Drop the function into your ~/.bashrc or a shared utilities file that you source in your scripts. Because it only uses POSIX‑compatible builtins (basename, date, mv) it works on any modern Linux box and on macOS with bash installed via Homebrew.
Testing the function
Here’s a quick sanity check you can run in a throwaway directory:
mkdir -p /tmp/safe_move_test/src /tmp/safe_move_test/dest
# Create a file with known content
echo "hello world" > /tmp/safe_move_test/src/test.log
# First move – should succeed with original name
safe_move /tmp/safe_move_test/src/test.log /tmp/safe_move_test/dest
ls -1 /tmp/safe_move_test/dest
# Create another file with the same name in the source
echo "second" > /tmp/safe_move_test/src/test.log
# Second move – should rename with timestamp
safe_move /tmp/safe_move_test/src/test.log /tmp/safe_move_test/dest
ls -1 /tmp/safe_move_test/dest
# Clean up
rm -rf /tmp/safe_move_test
Running this script should show the first file as test.log and the second as something like test_20240925153045.log. Feel free to replace the timestamp logic with a UUID to see the alternate behavior.
Closing thoughts
Having a small, well‑tested utility like this saves me from repetitive boilerplate and reduces the chance of silent data loss in automated pipelines. It’s a concrete example of how a few defensive lines can turn a risky one‑liner into a reliable building block for larger workflows.