#!/bin/bash # bak - move file to file.bak, rotating any existing backup chain # # Usage: # bak [options] [--] [file ...] # # Options: # -v, --verbose Print filenames as they are backed up # -n, --dry-run Simulate actions without making changes # -h, --help Show help message # # Backs up files by moving (renaming) them from "file.ext"->"file.ext.bak", # "file.ext.bak"->"file.ext.bak.bak", etc. which ensures the file with a single # ".bak" is always the most recent version _bak() ( 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="bak" ;; esac local args=() # Positional args 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${r} to ${s}file.bak${r}" echo "SYNOPSIS" echo " $SCRIPT_NAME [-v | -n] [--] [${s}file${r} ${s}...${r}]" echo " $SCRIPT_NAME -h" echo "OPTIONS" echo " -v, --verbose Print filenames as they are backed up" 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, mv failed)" echo " 2 Usage error (unknown option)" } _error() { echo "[ERR][$SCRIPT_NAME] $*" >&2; } # True if verbose flag is set _verbose() { [ "$verbose" = true ]; } # True if dry-run flag is set _dry_run() { [ "$dry_run" = true ]; } # Recursive backup function which ultimately calls mv _backup() { local file="$1" # Helper function to handle dry-run and verbose output _move_file() { local src="$1" local dest="$2" if [ ! -e "$src" ]; then _error "$src: No such file" 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 neither the file nor its backup exists, error out if [ ! -e "$file" ] && [ ! -e "$file.bak" ]; then _error "$file: No such file" return 1 fi # If a backup already exists, recursively rotate it if [ -e "$file.bak" ]; then _backup "$file.bak" || return 1 fi # Move the original file to its backup _move_file "$file" "$file.bak" || 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 --) shift # discard -- while [ $# -gt 0 ]; do args+=("$1") shift done ;; # Unknown option -*) _error "Unknown argument '$1'. Run \`$SCRIPT_NAME -h\` for usage" return 2 ;; # Positional argument *) 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 _backup "$f" done ) _bak "$@" __bak_rc=$? unset -f _bak if [ -n "${BASH_SOURCE[0]}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then eval "unset __bak_rc; return $__bak_rc" fi eval "unset __bak_rc; exit $__bak_rc"