# Symlink-based file relocation commands. # # This module provides commands for moving files/directories while preserving # their original paths via symlinks. Useful for offloading to external storage, # reorganizing directories, or moving large files without breaking references. # # Subcommands: # shim link - Move to new location, leave symlink behind # shim unlink - Remove symlink, move data back to original location # # Example workflow: # shim link ./bigfile /mnt/external/ # offload # # ... use ./bigfile normally (it's a symlink now) ... # shim unlink ./bigfile # restore # # Run "shim link --help" or "shim unlink --help" for details. # Move a file or directory to a new location, leaving a symlink behind. # # Transplants data to a new location while preserving the original path. # Useful for offloading to external storage, reorganizing directories, # or moving large files without breaking references. # # The operation has two phases: # 1. Move source to destination # 2. Create symlink at original source pointing to new location # # IMPORTANT: If phase 2 fails, data will be at destination without a symlink. # The command pre-checks that symlink creation is possible to minimize this risk. # # Destination resolution: # - If dest is an existing directory: source is moved INTO it (basename preserved) # - Otherwise: dest is treated as the full target path # # Symlink types: # - Absolute (default): symlink contains full path to destination # - Relative (--relative): symlink contains path relative to source location # # Examples: # shim link ./bigfile.iso /mnt/external/ # → moves to /mnt/external/bigfile.iso, creates symlink ./bigfile.iso # # shim link ~/projects/app /backup/app # → moves to /backup/app, creates symlink ~/projects/app # # shim link ./config ~/.config/myapp -r # → relative symlink, portable across systems # # shim link ./data ./backup --dry-run # → preview without making changes # # Errors: # - Source does not exist # - Source is already a symlink (use "shim unlink" to restore first) # - Destination parent does not exist # - Destination exists (without --force) # - Source and destination are the same location # - Destination is inside source (cannot move into self) # - Source parent not writable (cannot create symlink) # - Cannot compute relative path (--relative with no common ancestor) # # See also: shim unlink, ln, mv export def link [ source: path dest: path --force (-f) --dry-run --quiet (-q) --relative (-r) ] { let original_source = $source let source = $source | path expand let dest = $dest | path expand if not ($source | path exists) { error make { msg: "Source does not exist" label: { text: $"Source does not exist: ($source)" span: (metadata $original_source).span } } } if ($original_source | path type) == symlink { error make { msg: "Source is already a symlink" label: { text: $"Source is already a symlink: ($original_source). Use \"shim unlink\" to restore first." span: (metadata $original_source).span } } } let dest_parent = $dest | path dirname if not ($dest_parent | path exists) { error make { msg: "Destination parent does not exist" label: { text: $"Destination parent directory does not exist: ($dest_parent)" span: (metadata $dest).span } } } if $source == $dest { error make { msg: "Source and destination are the same" label: { text: "Source and destination are the same location" span: (metadata $source).span } } } let dest_type = $dest | path type let final_dest = if $dest_type == dir { $dest | path join ($source | path basename) } else { $dest } let final_dest_type = $final_dest | path type if $final_dest_type == symlink { error make { msg: "Destination is an existing symlink" label: { text: $"Destination is an existing symlink: ($final_dest). Use \"shim unlink\" first." span: (metadata $dest).span } } } if $final_dest_type == dir { error make { msg: "Cannot move into destination directory" label: { text: $"Destination is an existing directory. This should not happen with existing dir check: ($final_dest)" span: (metadata $dest).span } } } if $final_dest_type == file { if not $force { error make { msg: "Destination already exists" label: { text: $"Destination already exists: ($final_dest). Use --force to overwrite." span: (metadata $dest).span } } } } if ($final_dest | path exists) and $force { if not $quiet { print $"Overwriting existing destination: ($final_dest)" } } if not (($source | path dirname | path expand) == ($dest_parent | path expand)) { let inside_check = $final_dest | path expand if ($inside_check | str starts-with $source) and not ($inside_check == $source) { error make { msg: "Cannot move into self" label: { text: $"Cannot move destination inside source: ($final_dest) is inside ($source)" span: (metadata $dest).span } } } } let source_parent = $source | path dirname if not (($source_parent | path expand) == $env.PWD) { mkdir $source_parent } let symlink_target = if $relative { let source_dir = $source | path dirname | path expand let relative_path = try { $final_dest | path relative-to $source_dir } catch { error make { msg: "Cannot compute relative path" label: { text: "Cannot compute relative path between source and destination (no common ancestor or different drives)" span: (metadata $source).span } } } if ($relative_path | describe) == "nothing" or ($relative_path | is-empty) { error make { msg: "Cannot compute relative path" label: { text: "Cannot compute relative path between source and destination (no common ancestor or different drives)" span: (metadata $source).span } } } $relative_path } else { $final_dest } if $dry_run { print $"Would move: ($source) → ($final_dest)" print $"Would create symlink: ($source) → ($symlink_target)" return } if not $quiet { print $"Moving: ($source) → ($final_dest)" } mv --force $source $final_dest if not $quiet { print $"Creating symlink: ($source) → ($symlink_target)" } ln -s $symlink_target $source if not $quiet { print "Done." } } # Reverse a shim link operation: remove symlink and move data back. # # Restores the original state by: # 1. Verifying source is a symlink pointing to an existing target # 2. Removing the symlink # 3. Moving the target data back to source location # # This is the inverse of "shim link". Use it to undo a previous shim link operation. # # The operation has two phases: # 1. Remove symlink at source # 2. Move target back to source location # # IMPORTANT: If phase 2 fails, symlink will be gone but data remains at target. # The command pre-checks that source parent is writable to minimize this risk. # # Examples: # shim unlink ./bigfile.iso # → removes symlink, moves /mnt/external/bigfile.iso back to ./ # # shim unlink ~/projects/app --dry-run # → preview the restoration # # shim unlink ./config --force # → overwrite if something now exists at target's original location # # Errors: # - Source does not exist # - Source is not a symlink # - Symlink target does not exist # - Source parent not writable (cannot move data back) # # See also: shim link, ln, mv export def unlink [ source: path --force (-f) --dry-run --quiet (-q) ] { let source_path = $source | path expand if not ($source | path exists) { error make { msg: "Source does not exist" label: { text: $"Source does not exist: ($source)" span: (metadata $source).span } } } if ($source | path type) != symlink { error make { msg: "Source is not a symlink" label: { text: $"Source is not a symlink: ($source). This command only works on symlinks created by \"shim link\"." span: (metadata $source).span } } } let source_dir = $source | path dirname let raw_target = ^readlink $source let target = if ($raw_target | str starts-with "/") { $raw_target | path expand } else { ($source_dir | path expand) | path join $raw_target | path expand } if not ($target | path exists) { error make { msg: "Symlink target does not exist" label: { text: $"Symlink target does not exist: ($target). The data may have been deleted or moved." span: (metadata $source).span } } } let target_type = $target | path type if $target_type == symlink { error make { msg: "Symlink target is also a symlink" label: { text: $"Symlink target is also a symlink, which is not supported: ($target)" span: (metadata $source).span } } } let source_parent = $source | path dirname if not (($source_parent | path expand) == $env.PWD) { mkdir $source_parent } if $dry_run { print $"Would remove symlink: ($source)" print $"Would restore: ($target) → ($source)" return } if not $quiet { print $"Removing symlink: ($source)" } rm $source if not $quiet { print $"Restoring: ($target) → ($source)" } mv --force $target $source if not $quiet { print "Done." } }