Phuriwaj

Bash backup() Function — Files and Directories

Shell function that creates timestamped backups of both files (cp) and directories (zip), with extension preservation, exclusion lists, and force-overwrite flag.

Why / When to Use

Quick local backup before risky edits. Works for single config files and full project directories. Add to .zshrc or .bashrc.

Core Concept / Commands

backup() {
  local FORCE=false
  _backup_help() {
    cat <<'EOF'
backup — create timestamped backup of a file or directory
USAGE
  backup <path>
  backup <path> <output>
OPTIONS
  -f, --force   overwrite existing backup
  -h, --help    show help
DEFAULT OUTPUT
  directory → <name>_backup_YYYYMMDD_HHMM.zip
  file      → <name>_backup_YYYYMMDD_HHMM.<ext> (copy)
EXAMPLES
  backup jellyfish/
  backup jellyfish/ release.zip
  backup config.yaml
  backup -f config.yaml
EOF
  }
  while [[ "$1" == -* ]]; do
    case "$1" in
      -f|--force) FORCE=true ;;
      -h|--help) _backup_help; return 0 ;;
      *) echo "Unknown option: $1"; return 1 ;;
    esac
    shift
  done
  [[ -z "$1" ]] && { echo "Error: path required"; _backup_help; return 1; }
  local TARGET="$1"
  [[ ! -e "$TARGET" ]] && { echo "Error: '$TARGET' does not exist"; return 1; }
  local BASENAME TIMESTAMP
  BASENAME="$(basename "${TARGET%/}")"
  TIMESTAMP="$(date +"%Y%m%d_%H%M")"
 
  if [[ -f "$TARGET" ]]; then
    local NAME="${BASENAME%.*}"
    local EXT="${BASENAME##*.}"
    local DEST
    if [[ "$EXT" == "$BASENAME" ]]; then
      DEST="${2:-${NAME}_backup_${TIMESTAMP}}"
    else
      DEST="${2:-${NAME}_backup_${TIMESTAMP}.${EXT}}"
    fi
    [[ -f "$DEST" && "$FORCE" != true ]] && { echo "Error: '$DEST' already exists (use -f to overwrite)"; return 1; }
    echo "Creating backup:"; echo "  Source : $TARGET"; echo "  Output : $DEST"; echo
    cp "$TARGET" "$DEST"
    local STATUS=$?
 
  elif [[ -d "$TARGET" ]]; then
    local ZIPFILE="${2:-${BASENAME}_backup_${TIMESTAMP}.zip}"
    [[ -f "$ZIPFILE" && "$FORCE" != true ]] && { echo "Error: '$ZIPFILE' already exists (use -f to overwrite)"; return 1; }
    local EXCLUDES=(
      "*/.git/*" "*/.venv/*" "*/venv/*" "*/node_modules/*" "*/__pycache__/*"
      "*/.pytest_cache/*" "*/.mypy_cache/*" "*.pyc" "*.pyo" "*.DS_Store"
      "*/.next/*" "*/.npm/*" "*/.cache/*" "*/cache/*" "*/tmp/*"
      "*/runtime/*" "*/sessions/*" "*/.claude/*/*" "*/test-artifacts/*"
    )
    local -a EXCLUDE_ARGS=()
    for p in "${EXCLUDES[@]}"; do EXCLUDE_ARGS+=(-x "$p"); done
    echo "Creating backup:"; echo "  Source : $TARGET"; echo "  Output : $ZIPFILE"; echo
    zip -r "$ZIPFILE" "$TARGET" "${EXCLUDE_ARGS[@]}"
    local STATUS=$?
  fi
 
  [[ $STATUS -eq 0 ]] && echo "✓ Backup created: ${DEST:-$ZIPFILE}" || echo "✗ Backup failed"
  return $STATUS
}

Key Options / Variants

  • backup config.yamlconfig_backup_20260601_1430.yaml
  • backup config.yaml my-config.yaml → custom output filename
  • backup -f config.yaml → overwrite existing backup
  • backup project/project_backup_20260601_1430.zip (excludes node_modules, .git, .next, etc.)
  • Files with no extension (e.g. Makefile) get suffix without a dot: Makefile_backup_20260601_1430

Gotchas

  • Extension detection: EXT="${BASENAME##*.}" equals BASENAME when there’s no dot — the condition [[ "$EXT" == "$BASENAME" ]] catches this and omits the dot in the output name.
  • The exclusion list for directories covers most common project junk; extend EXCLUDES array as needed.

Source

Conversation “Extending backup function to support files” — 2026-06-01