trash

phroessler was looking for a safe rm replacement and found this: trash.tcl

I made some improvements to it:

#!/usr/bin/tclsh
## From https://gist.githubusercontent.com/adamnew123456/1321420/raw/560ae2253e6daa7d08a97aa16ca853d968f628a7/trash.tcl
#    Copyright 2026 Philipp Roessler
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
set filepath "$env(HOME)/.local/share/Trash/files"
set infopath "$env(HOME)/.local/share/Trash/info"

proc basename {path} {
  return [file tail $path]
}

proc trash {path} {
    set bp [basename $path]
    set fp [open "$::infopath/$bp.trashinfo" "w"]
    puts $fp "\[Trash Info\]"
    puts $fp "Path=[pwd]/[basename $path]"
    puts $fp "DeletionDate=[clock format [clock seconds] -format "%Y-%m-%dT%T"]"
    close $fp
    file rename $path "$::filepath/[basename $path]"
}

proc untrash {fname} {
    set fp [open "$::infopath/$fname.trashinfo"]
    set txt [read $fp]
    foreach line [split $txt "\n"] {
        set re [string compare [string range $line 0 4] "Path="]
        if {$re == 0} {
            set path [lindex [split $line "="] 1]
            break
        }
    }
    close $fp
    file delete "$::infopath/$fname.trashinfo"
    file rename "$::filepath/$fname" $path
}

proc returnAllFiles {aPath} {
  # Return sorted list of hidden and unhidden files and folders except of "." and ".."
  set lFileList [lsort [lsearch -all -inline -not -regexp [list {*}[glob -nocomplain -directory "$aPath" -tails -types hidden -- "*"] {*}[glob -nocomplain -directory "$aPath" -tails -- "*"]] {^[.]{1,2}$}]]
  return $lFileList
}

proc view {} {
    foreach lFile [returnAllFiles "$::filepath"] {
      puts [basename $lFile]
    }
}

proc clean {fname} {
    file delete "$::infopath/$fname.trashinfo"
    file delete -force "$::filepath/$fname"
}

proc help {} {
    puts "Trash - A simple program to manage your trash bin"
    puts "Usage trash \[-t|-u|-e|-l|-c|-n|-h\] \[files...\]"
    puts "  -t - Trash all listed files"
    puts "  -u - Untrash all listed files"
    puts "  -e - Untrash (evacuate) all files"
    puts "  -l - List all files"
    puts "  -c - Permanently delete (clean) all listed files"
    puts "  -n - Permanently delete all files (nuke)"
    puts "  -h - Print this message"
}

if {$argc < 1} {
    help
    exit
}

set option [lindex $argv 0]
set filename_suffix [join "[clock milli] [string range [expr rand ()] 2 end]" "_"]
if {[string compare $option "-t"] == 0} {
    foreach item [lrange $argv 1 end] {
        set new_name [join [list [string trimright "$item" "/"] "$filename_suffix"] "_"]
        file rename $item $new_name
        trash $new_name
    }
} elseif {[string compare $option "-u"] == 0} {
    foreach item [lrange $argv 1 end] {
        untrash $item
    }
} elseif {[string compare $option "-e"] == 0} {
    foreach item [returnAllFiles "$::filepath"] {
        untrash [file tail $item]
    }
} elseif {[string compare $option "-l"] == 0} {
    view
} elseif {[string compare $option "-c"] == 0} {
    foreach item [lrange $argv 1 end] {
        clean $item
    }
} elseif {[string compare $option "-n"] == 0} {
    foreach item [returnAllFiles "$::filepath"] {
      clean [file tail $item]
    }
} else {
    help
}