#!/bin/bash # unbak - move file.bak to file, recursively restoring a backup chain # # Usage: # unbak [options] [--] [file ...] # command | unbak # # Options: # -v, --verbose Print filenames as they are restored # -n, --dry-run Simulate actions without making changes # -h, --help Show help message # # Restores backed-up files by moving (renaming) them from "file.ext.bak" -> # "file.ext", avoiding overwriting files by default _unbak() ( local SCRIPT_NAME; SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")" case "${BASH_SOURCE[0]}" in /dev/*|/proc/*) SCRIPT_NAME="" ;; esac case "$SCRIPT_NAME" in ""|bash|sh|zsh|dash) SCRIPT_NAME="unbak" ;; esac local args=() # Positional arguments # Read piped/redirected input as an array of lines. The `|| [ -n "$line" ]` # clause appends the final line when stdin lacks a trailing newline local line [ -t 0 ] || while IFS= read -r line || [ -n "$line" ]; do args+=("$line"); done local verbose=false local dry_run=false # Print usage (help) info _show_help() { # Underline only if output is a terminal (not a pipe or file) local s; [ -t 1 ] && s=$'\033[4m' local r; [ -t 1 ] && r=$'\033[24m' echo "NAME" echo " $SCRIPT_NAME - move ${s}file.bak${r} to ${s}file${r}" echo "SYNOPSIS" echo " $SCRIPT_NAME [-v | -n] [--] [${s}file${r} ${s}...${r}]" echo " ${s}command${r} | $SCRIPT_NAME" echo " $SCRIPT_NAME -h" echo "DESCRIPTION" echo " Restores backed-up files by moving them from ${s}file.bak${r} to" echo " ${s}file${r}. Refuses to overwrite an existing ${s}file${r}." echo "" echo " Accepts filenames as positional arguments, as newline-delimited" echo " input on stdin, or both (stdin is read when it is not a TTY)." echo "" echo " Recursively restores an entire backup chain:" echo " file.bak.bak.bak -> file.bak.bak -> file.bak -> file" echo " The deepest backup is restored first; if any intermediate step" echo " would overwrite an existing file, the operation is aborted." echo "OPTIONS" echo " -v, --verbose Print filenames as they are restored" echo " -n, --dry-run Simulate actions without making changes" echo " -h, --help Show this help message" echo "EXIT STATUS" echo " 0 Success" echo " 1 Runtime failure (source file missing, destination exists, mv failed)" echo " 2 Usage error (unknown option)" } _error() { echo "[ERR][$SCRIPT_NAME] $*" >&2; } # Return true if verbose flag is set _verbose() { [ "$verbose" = true ]; } # Return true if dry-run flag is set _dry_run() { [ "$dry_run" = true ]; } # Recursive restore function which ultimately calls mv _restore() { local file="$1" # Remove one trailing ".bak" to compute the destination local base="${file%".bak"}" if [ ! -e "$file" ]; then _error "$file: No such file" return 1 fi # Only check if the destination exists when restoring the final file # If base ends in ".bak", then it's an intermediate backup file in the chain case "$base" in *.bak) # Intermediate backup: no need to check existence ;; *) if [ -e "$base" ]; then _error "$base: File already exists, skipping" return 1 fi ;; esac # Helper function to move files with dry-run and verbose support _move_file() { local src="$1" local dest="$2" if [ ! -e "$src" ]; then _error "$src: No such file" return 1 fi if [ -e "$dest" ]; then _error "$dest: File already exists, skipping" return 1 fi if _dry_run; then echo "$SCRIPT_NAME: Would move: $src -> $dest" else if _verbose; then echo "$SCRIPT_NAME: Moving: $src -> $dest" fi mv "$src" "$dest" fi } # If there is an older backup (one more ".bak" appended), # restore it first so that the oldest backup in the chain gets restored if [ -e "$file.bak" ]; then _restore "$file.bak" || return 1 fi _move_file "$file" "$base" || return 1 } _expand_short_opts() { # $1 = string of short-opt letters that take a value (e.g. "nXHd"); "" for flag-only scripts # $2..$N = "$@" # Populates _EXPANDED; caller does: set -- "${_EXPANDED[@]}"; unset _EXPANDED local value_opts="$1"; shift _EXPANDED=() local passthru="" local arg local rest local c for arg in "$@"; do if [ -n "$passthru" ]; then _EXPANDED+=("$arg"); continue; fi case "$arg" in --) passthru=1; _EXPANDED+=("$arg") ;; --*|-|"") _EXPANDED+=("$arg") ;; -[a-zA-Z]?*) rest="${arg#-}" while [ -n "$rest" ]; do c="${rest%"${rest#?}"}"; rest="${rest#?}" _EXPANDED+=("-$c") case "$value_opts" in *"$c"*) [ -n "$rest" ] && _EXPANDED+=("$rest") rest="" ;; esac done ;; *) _EXPANDED+=("$arg") ;; esac done } # Clean up inner functions when returning _expand_short_opts "" "$@" set -- "${_EXPANDED[@]}"; unset _EXPANDED # Parse parameters while [ $# -gt 0 ]; do case "$1" in -v|--verbose) verbose=true shift ;; -n|--dry-run) dry_run=true shift ;; -h|--help) _show_help return 0 ;; # End of options, parse remainder as positional arguments --) shift # discard -- while [ $# -gt 0 ]; do args+=("$1") shift done ;; -*) _error "Unknown argument '$1'. Run \`$SCRIPT_NAME -h\` for usage" return 2 ;; *) args+=("$1") shift ;; esac done # Show help if no filenames provided if [ ${#args[@]} -eq 0 ]; then _show_help return 0 fi # Process each argument for f in "${args[@]}"; do _restore "$f" done ) _unbak "$@" __unbak_rc=$? unset -f _unbak if [ -n "${BASH_SOURCE[0]}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then eval "unset __unbak_rc; return $__unbak_rc" fi eval "unset __unbak_rc; exit $__unbak_rc"