repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
nushell/modules/shim.nu
Raw# 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."
}
}