#!/usr/bin/env bash # blender-mv: A smart file/directory mover that automatically updates Blender paths set -e print_usage() { echo "Usage: blender-mv [OPTIONS] SOURCE DEST" echo "" echo "Options:" echo " --search-project Scan this specific project directory for .blend files" echo " to update (useful if you move an external asset folder," echo " and old .blend files referencing it are elsewhere)." echo "" echo "Example: blender-mv ./old_folder ./new_folder" } SEARCH_PROJECT="" # Parse arguments while [[ "$#" -gt 0 ]]; do case $1 in --search-project) SEARCH_PROJECT="$2"; shift 2 ;; -h|--help) print_usage; exit 0 ;; *) if [[ -z "$SOURCE" ]]; then SOURCE="$1" elif [[ -z "$DEST" ]]; then DEST="$1" else echo "Unknown parameter passed: $1" print_usage exit 1 fi shift ;; esac done if [[ -z "$SOURCE" || -z "$DEST" ]]; then echo "Error: Both SOURCE and DEST must be provided." print_usage exit 1 fi if [[ ! -e "$SOURCE" ]]; then echo "Error: Source path '$SOURCE' does not exist." exit 1 fi # Function to get absolute normalized path (handles missing destination gracefully) get_abs_path() { python3 -c 'import os,sys; print(os.path.normpath(os.path.abspath(sys.argv[1])))' "$1" 2>/dev/null } # Resolve source before moving ABS_SOURCE=$(get_abs_path "$SOURCE") # Determine resulting target path if [[ -d "$DEST" ]]; then # If DEST is an existing dir, standard mv puts SOURCE inside it BASENAME_SOURCE=$(basename "$SOURCE") ABS_DEST=$(get_abs_path "$DEST/$BASENAME_SOURCE") else # Placed exactly at DEST (rename or target dir doesn't exist yet) ABS_DEST=$(get_abs_path "$DEST") fi echo "Moving: $SOURCE -> $DEST" mv "$SOURCE" "$DEST" echo "Replacing paths in Blender's recent-files lists..." # Find all recent-files.txt across Blender versions and replace the string find "$HOME/Library/Application Support/Blender" -type f -name "recent-files.txt" 2>/dev/null | while read -r RECENT_FILE; do # Using sed with alternative delimiter to avoid path interference sed -i '' "s|${ABS_SOURCE}|${ABS_DEST}|g" "$RECENT_FILE" done # Prepare the embedded Python payload to update blend paths TMP_PY=$(mktemp /tmp/blender_update_paths.XXXXXX.py) cat << 'EOF' > "$TMP_PY" import bpy import sys def replace_in_prop(prop, old_path, new_path): if prop and isinstance(prop, str) and old_path in prop: return prop.replace(old_path, new_path) return prop def run(): # Args are passed after `--` if '--' not in sys.argv: return idx = sys.argv.index('--') if len(sys.argv) <= idx + 2: return old_path = sys.argv[idx + 1] new_path = sys.argv[idx + 2] changed = False # 1. Libraries (Linked objects/collections) for lib in bpy.data.libraries: new_fp = replace_in_prop(lib.filepath, old_path, new_path) if new_fp != lib.filepath: lib.filepath = new_fp changed = True # 2. Images (Textures, Env Maps) for img in bpy.data.images: new_fp = replace_in_prop(img.filepath, old_path, new_path) if new_fp != img.filepath: img.filepath = new_fp changed = True # 3. Cache files (Alembic/VDB) for cache in bpy.data.cache_files: new_fp = replace_in_prop(cache.filepath, old_path, new_path) if new_fp != cache.filepath: cache.filepath = new_fp changed = True # 4. Fonts for font in bpy.data.fonts: new_fp = replace_in_prop(font.filepath, old_path, new_path) if new_fp != font.filepath: font.filepath = new_fp changed = True # 5. Video strips for scene in bpy.data.scenes: if scene.sequence_editor: for strip in scene.sequence_editor.sequences_all: if strip.type in ('MOVIE', 'IMAGE'): new_fp = replace_in_prop(strip.filepath, old_path, new_path) if new_fp != strip.filepath: strip.filepath = new_fp changed = True if changed: print(f"[*] Updated paths in {bpy.data.filepath}") bpy.ops.wm.save_mainfile() try: run() except Exception as e: print(f"[!] Error updating blend file paths: {e}") EOF # Determine what to search for blend files if [[ -n "$SEARCH_PROJECT" ]]; then SEARCH_ROOT=$(get_abs_path "$SEARCH_PROJECT") echo "Scanning project root '$SEARCH_ROOT' for .blend files..." elif [[ -d "$ABS_DEST" ]]; then SEARCH_ROOT="$ABS_DEST" echo "Scanning moved directory for .blend files..." elif [[ "$ABS_DEST" == *.blend ]]; then SEARCH_ROOT="$ABS_DEST" echo "Updating moved .blend file..." else echo "No project root defined and moved object is not a directory or .blend file." echo "Finished." rm -f "$TMP_PY" exit 0 fi # Execute headless blender on all found blend files if [[ -d "$SEARCH_ROOT" ]]; then find "$SEARCH_ROOT" -type f -name "*.blend" -print0 | while IFS= read -r -d '' BLEND_FILE; do blender -b "$BLEND_FILE" -P "$TMP_PY" -- "$ABS_SOURCE" "$ABS_DEST" >/dev/null 2>&1 || echo "Warning: Failed to process $BLEND_FILE" done elif [[ -f "$SEARCH_ROOT" && "$SEARCH_ROOT" == *.blend ]]; then blender -b "$SEARCH_ROOT" -P "$TMP_PY" -- "$ABS_SOURCE" "$ABS_DEST" >/dev/null 2>&1 || echo "Warning: Failed to process $SEARCH_ROOT" fi rm -f "$TMP_PY" echo "Success: Move complete and paths updated."