Fix bugs and more work on the command line
This commit is contained in:
		
							
								
								
									
										357
									
								
								clash/clash.lisp
									
									
									
									
									
								
							
							
						
						
									
										357
									
								
								clash/clash.lisp
									
									
									
									
									
								
							| @ -40,10 +40,11 @@ | ||||
|             :initarg :message | ||||
|             :documentation "A message describing the error.")) | ||||
|   (:report (lambda (condition stream) | ||||
|              (with-slots (source pos message) condition | ||||
|                (format | ||||
|                 stream "Failed to parse date ~S~@[ at position ~A~]: ~A" | ||||
|                 source pos message)))) | ||||
|              (format | ||||
|               stream "Failed to parse date ~S~@[ at position ~A~]: ~A" | ||||
|               (date-parse-error-source condition) | ||||
|               (date-parse-error-position condition) | ||||
|               (date-parse-error-message condition)))) | ||||
|   (:documentation "A condition representing a failure in parsing a date range.")) | ||||
|  | ||||
| (defparameter *month-conversion-table* | ||||
| @ -234,6 +235,23 @@ date ranges..")) | ||||
| (defun clingon-filtering-options () | ||||
|   "Return some options that can be used by many commands for filtering." | ||||
|   (list | ||||
|    (clingon:make-option | ||||
|     :list/filepath | ||||
|     :key :extra-trashes | ||||
|     :description "include additional trashes" | ||||
|     :short-name #\c | ||||
|     :long-name "include-trash") | ||||
|    (clingon:make-option | ||||
|     :list/filepath | ||||
|     :key :ignored-trashes | ||||
|     :description "ignore the given trash directory" | ||||
|     :long-name "ignore-trash") | ||||
|    (clingon:make-option | ||||
|     :flag | ||||
|     :key :only-explicit-dirs | ||||
|     :description "only use trash directories supplied with -c" | ||||
|     :short-name #\E | ||||
|     :long-name "explicit-trashes-only") | ||||
|    (clingon:make-option | ||||
|     :flag | ||||
|     :key :print-format-info | ||||
| @ -329,9 +347,11 @@ string." | ||||
|  | ||||
| (defun list-nonexcluded-trash-dirs (cmd) | ||||
|   "Return a list of all trash directories, except those excluded by CMD." | ||||
|   (append (set-difference (cl-xdg-trash:list-trash-directories) | ||||
|                           (clingon:getopt cmd :ignored-trashes) | ||||
|                           :test #'uiop:pathname-equal) | ||||
|   (append (unless (clingon:getopt cmd :only-explicit-dirs) | ||||
|             (set-difference (cl-xdg-trash:list-trash-directories) | ||||
|                             (mapcar #'uiop:ensure-directory-pathname | ||||
|                                     (clingon:getopt cmd :ignored-trashes)) | ||||
|                             :test #'uiop:pathname-equal)) | ||||
|           (mapcar #'ensure-nonwild-pathname | ||||
|                   (clingon:getopt cmd :extra-trashes)))) | ||||
|  | ||||
| @ -371,6 +391,20 @@ string." | ||||
|  | ||||
|  | ||||
| ;; Formatting | ||||
| (defun format-size (count &optional base-two (places 2)) | ||||
|   "Pretty print COUNT, which is a number of bytes. This will append metric | ||||
| suffixes as necessary. If BASE-TWO is non-nil, use MiB, GiB, etc. suffixes | ||||
| instead." | ||||
|   (if (not count) | ||||
|       "N/A" ;; if find the size of something failed | ||||
|       (let* ((base (if base-two 1024 1000)) | ||||
|              (power (min 10 (floor (if (zerop count) 0 (log count base)))))) | ||||
|         (if (zerop power) | ||||
|             (format nil "~DB" count) | ||||
|             (format nil "~,VF~[~:[k~;K~]~:*~;M~;G~;T~;P~;E~;Z~;Y~;R~;Q~]~@[i~]B" | ||||
|                     places (/ count (expt base power)) | ||||
|                     (1- power) base-two))))) | ||||
|  | ||||
| (defparameter *trashinfo-formatters* | ||||
|   `((#\# :index | ||||
|          "the index of the current file (used when prompting for files)") | ||||
| @ -399,11 +433,24 @@ string." | ||||
|              stream (trashinfo-deletion-date info) | ||||
|              :format local-time:+asctime-format+)) | ||||
|          "the (t)ime the file was trashed (pretty-printed local time)") | ||||
|     (#\t ,(lambda (stream info) | ||||
|     (#\s ,(lambda (stream info) | ||||
|             (format stream "~A" (trashed-file-size | ||||
|                                  (trashinfo-trash-directory info) | ||||
|                                  (trashinfo-name info)))) | ||||
|          "the file's (s)size") | ||||
|          "the file's (s)size in bytes") | ||||
|     (#\h ,(lambda (stream info) | ||||
|             (format stream "~A" | ||||
|                     (format-size (trashed-file-size | ||||
|                                   (trashinfo-trash-directory info) | ||||
|                                   (trashinfo-name info))))) | ||||
|          "the file's size with a (h)uman readable suffix (powers of 10)") | ||||
|     (#\H ,(lambda (stream info) | ||||
|             (format stream "~A" | ||||
|                     (format-size (trashed-file-size | ||||
|                                   (trashinfo-trash-directory info) | ||||
|                                   (trashinfo-name info)) | ||||
|                                  t))) | ||||
|          "the file's size with a (H)uman readable suffix (power of 2)") | ||||
|     (#\% ,(lambda (stream info) | ||||
|             (declare (ignore info)) | ||||
|             (format stream "%")) | ||||
| @ -539,14 +586,7 @@ The recognizes printf-style sequences are (parenthesis denote the mnemonic):~%") | ||||
|   "Return options for the \"list\" subcommand." | ||||
|   (append | ||||
|    (clingon-filtering-options) | ||||
|    (clingon-sort-options) | ||||
|    (list | ||||
|     (clingon:make-option | ||||
|      :list/filepath | ||||
|      :key :extra-trashes | ||||
|      :description "include additional trashes" | ||||
|      :short-name #\c | ||||
|      :long-name "include-trash")))) | ||||
|    (clingon-sort-options))) | ||||
|  | ||||
| (defun list/command () | ||||
|   "Return the Clingon command for the \"list\" subcommand." | ||||
| @ -600,10 +640,83 @@ The recognizes printf-style sequences are (parenthesis denote the mnemonic):~%") | ||||
|  | ||||
|  | ||||
| ;; Restore command | ||||
| (declaim (inline single-item-list-p)) | ||||
| (defun single-item-list-p (list) | ||||
|   "Return non-nil if LIST has only one thing." | ||||
|   (and list (null (cdr list)))) | ||||
|  | ||||
| (defun prompt-for-index (stream action max &optional allow-many) | ||||
|   "Prompt the user for an index between 1 and MAX to restore. With ALLOW-MANY, | ||||
| return a list of many indices instead." | ||||
|   (when (zerop max) | ||||
|     (error "Nothing found...")) | ||||
|   (format stream "~&Select ~:[indices~;index~] to ~A: " | ||||
|           (or (eql 1 max) (not allow-many)) action) | ||||
|   (let ((resp-string (read-line stream nil)) | ||||
|         (seperators '(#\Space #\Tab #\,))) | ||||
|     (unless resp-string | ||||
|       (error "No number provided")) | ||||
|     (let ((parts (uiop:split-string resp-string | ||||
|                                     :separator seperators)) | ||||
|           (out (make-hash-table :test #'eql))) | ||||
|       (unless parts | ||||
|         (error "No number provided")) | ||||
|       (unless (or allow-many | ||||
|                   (single-item-list-p parts)) | ||||
|         (error "Only one item can be selected")) | ||||
|       (dolist (part parts) | ||||
|         (unless (every (lambda (c) (member c seperators)) part) | ||||
|           (let ((n (parse-integer part))) | ||||
|             (when (or (< n 1) | ||||
|                       (> n max)) | ||||
|               (error "Number ~A out of range [1,~A]" n max)) | ||||
|             (setf (gethash (1- n) out) t)))) | ||||
|       (loop for key being the hash-keys of out collect key)))) | ||||
|  | ||||
| (defun restore/handler (cmd) | ||||
|   "Handler for the \"restore\" subcommand." | ||||
|   (le)) | ||||
|   (if (clingon:getopt cmd :print-format-info) | ||||
|       (print-format-info t) | ||||
|       (let ((format (process-format-string (or (clingon:getopt cmd :format) | ||||
|                                                "%#: %t  %o\\n"))) | ||||
|             (infos (list-trashinfos-for-cmd cmd)) | ||||
|             (target (clingon:getopt cmd :target)) | ||||
|             (all (clingon:getopt cmd :all)) | ||||
|             (cli-indices (clingon:getopt cmd :indices))) | ||||
|         (when (and all target) | ||||
|           (error "Only one of -a and -t can be supplied")) | ||||
|         (cond | ||||
|           ((and (clingon:getopt cmd :dont-prompt-only-one) | ||||
|                 (single-item-list-p infos)) | ||||
|            (cl-xdg-trash:restore-file (car infos))) | ||||
|           ((clingon:getopt cmd :all) | ||||
|            (mapc #'cl-xdg-trash:restore-file infos)) | ||||
|           (cli-indices | ||||
|            (unless infos | ||||
|              (error "Nothing found...")) | ||||
|            (loop with sorted-arr = (sort-trashinfos-for-cmd | ||||
|                                     (coerce infos 'vector) cmd) | ||||
|                  for i in cli-indices | ||||
|                  when (or (> i (length sorted-arr)) | ||||
|                           (< i 1)) | ||||
|                    do (error "Index ~S out of bounds [1,~S]" | ||||
|                              i (length sorted-arr)) | ||||
|                  do (cl-xdg-trash:restore-file (aref sorted-arr (1- i))))) | ||||
|           (t | ||||
|            (let ((sorted-arr (coerce infos 'vector))) | ||||
|              (sort-trashinfos-for-cmd sorted-arr cmd) | ||||
|              (loop for info across sorted-arr | ||||
|                    for i upfrom 1 | ||||
|                    do (format-trashinfo t format info :index i)) | ||||
|              (let ((idxs (prompt-for-index t "restore" | ||||
|                                            (length sorted-arr) | ||||
|                                            (not target)))) | ||||
|                (if target | ||||
|                    (cl-xdg-trash:restore-file (aref sorted-arr (car idxs)) | ||||
|                                               :target target) | ||||
|                    (loop for i in idxs | ||||
|                          do (cl-xdg-trash:restore-file | ||||
|                              (aref sorted-arr i))))))))))) | ||||
|  | ||||
| (defun restore/options () | ||||
|   "Return options for the \"restore\" subcommand." | ||||
| @ -611,10 +724,16 @@ The recognizes printf-style sequences are (parenthesis denote the mnemonic):~%") | ||||
|    (clingon-filtering-options) | ||||
|    (clingon-sort-options) | ||||
|    (list | ||||
|     (clingon:make-option | ||||
|      :filepath | ||||
|      :key :target | ||||
|      :description "where path to restore the file (exclusive with -a)" | ||||
|      :short-name #\t | ||||
|      :long-name "target") | ||||
|     (clingon:make-option | ||||
|      :flag | ||||
|      :key :all | ||||
|      :description "restore all files that match the pattern" | ||||
|      :description "restore all files that match the pattern (exclusive with -t)" | ||||
|      :short-name #\a | ||||
|      :long-name "all") | ||||
|     (clingon:make-option | ||||
| @ -627,7 +746,7 @@ The recognizes printf-style sequences are (parenthesis denote the mnemonic):~%") | ||||
|     (clingon:make-option | ||||
|      :flag | ||||
|      :key :dont-prompt-only-one | ||||
|      :descrition "don't prompt if the pattern matches only one file" | ||||
|      :description "don't prompt if the pattern matches only one file" | ||||
|      :short-name #\O | ||||
|      :long-name "dont-prompt-only-one")))) | ||||
|  | ||||
| @ -635,23 +754,192 @@ The recognizes printf-style sequences are (parenthesis denote the mnemonic):~%") | ||||
|   "Rethrn the Clingon command for the \"restore\" subcommand." | ||||
|   (clingon:make-command | ||||
|    :name "restore" | ||||
|    :descrition "move files out of the trash" | ||||
|    :description "move files out of the trash" | ||||
|    :usage "[options] [pattern]" | ||||
|    :options (restore/options) | ||||
|    :handler #'restore/handler)) | ||||
|  | ||||
|  | ||||
| ;; Toplevel command | ||||
| (defun toplevel/options () | ||||
|   "Return the toplevel options list." | ||||
| ;; Empty command | ||||
| (defun prompt-yes-or-no (stream control &rest args) | ||||
|   "Prompt the user for a yes or no response." | ||||
|   (when control | ||||
|     (apply #'format stream control args) | ||||
|     (format stream "? (y/n) ")) | ||||
|   (let ((resp-string (read-line stream nil))) | ||||
|     (when resp-string | ||||
|       (member resp-string '("yes" "y" "1") :test #'equalp)))) | ||||
|  | ||||
| (defun prompt-to-empty (count quiet) | ||||
|   "Prompt for emptying the trash for the \"empty\" command. Specifically, used | ||||
| with -n and -a." | ||||
|   (prompt-yes-or-no | ||||
|    t "Really erase ~@[~*the above ~]~@[~*~A~:* ~]file~P" | ||||
|    (not quiet) (or quiet (/= 1 count)) count)) | ||||
|  | ||||
| (defun empty/handler (cmd) | ||||
|   "Handler for the \"empty\" subcommand." | ||||
|   (cond | ||||
|     ((clingon:getopt cmd :print-format-info) | ||||
|      (print-format-info t)) | ||||
|     (t | ||||
|      (let ((format (process-format-string | ||||
|                     (or (clingon:getopt cmd :format) | ||||
|                         "%#: %t  %o\\n"))) | ||||
|            (dry-run (clingon:getopt cmd :dry-run)) | ||||
|            (infos (coerce (list-trashinfos-for-cmd cmd) 'vector)) | ||||
|            (quiet (clingon:getopt cmd :quiet)) | ||||
|            (yes (clingon:getopt cmd :yes)) | ||||
|            (all (clingon:getopt cmd :all)) | ||||
|            (indices (clingon:getopt cmd :indices))) | ||||
|        (when (and yes (not (or all indices))) | ||||
|          (error "Found -y with neither -a nor -n, doing nothing")) | ||||
|        (when (and quiet (not (or all indices))) | ||||
|          (error "Found -q with neither -a nor -n, doing nothing")) | ||||
|        (when (and (zerop (length infos)) (not quiet)) | ||||
|          (error "Nothing found...")) | ||||
|        (unless all | ||||
|          (sort-trashinfos-for-cmd infos cmd)) | ||||
|        (unless (or yes quiet) | ||||
|          (loop for info across infos | ||||
|                for i upfrom 1 | ||||
|                when (or (not indices) (member i indices)) | ||||
|                  do (format-trashinfo t format info :index i))) | ||||
|        (cond | ||||
|          (all | ||||
|           (when (or yes (prompt-to-empty (length infos) quiet)) | ||||
|             (loop for info across infos | ||||
|                   do (cl-xdg-trash:empty-file info :dry-run dry-run)))) | ||||
|          (indices | ||||
|           (when (or yes (prompt-to-empty (length indices) quiet)) | ||||
|             (loop for i in indices | ||||
|                   when (or (< i 1) | ||||
|                            (> i (length infos))) | ||||
|                     do (error "Index ~A out of bounds [1,~A]" | ||||
|                               i (length infos)) | ||||
|                   else | ||||
|                     do (cl-xdg-trash:empty-file (aref infos (1- i)) | ||||
|                                                 :dry-run dry-run)))) | ||||
|          (t | ||||
|           (let ((index (prompt-for-index t "erase" (length infos) t))) | ||||
|             (dolist (i index) | ||||
|               (cl-xdg-trash:empty-file (aref infos i) :dry-run dry-run))))))))) | ||||
|  | ||||
| (defun empty/options () | ||||
|   "Return options for the \"empty\" subcommand." | ||||
|   (append | ||||
|    (clingon-filtering-options) | ||||
|    (clingon-sort-options) | ||||
|    (list | ||||
|     (clingon:make-option | ||||
|      :flag | ||||
|      :key :dry-run | ||||
|      :description "print what would happen without actually deleting anything" | ||||
|      :short-name #\D | ||||
|      :long-name "dry-run") | ||||
|     (clingon:make-option | ||||
|      :flag | ||||
|      :key :all | ||||
|      :description "erase all matching files" | ||||
|      :short-name #\a | ||||
|      :long-name "all") | ||||
|     (clingon:make-option | ||||
|      :list/integer | ||||
|      :key :indices | ||||
|      :description | ||||
|      "erase the Nth file that matched the pattern (after sorting)" | ||||
|      :short-name #\n | ||||
|      :long-name "nth") | ||||
|     (clingon:make-option | ||||
|      :flag | ||||
|      :key :yes | ||||
|      :description "erase files without prompting (implies -q)" | ||||
|      :short-name #\y | ||||
|      :long-name "yes") | ||||
|     (clingon:make-option | ||||
|      :flag | ||||
|      :key :quiet | ||||
|      :description "be less verbose" | ||||
|      :short-name #\q | ||||
|      :long-name "quiet")))) | ||||
|  | ||||
| (defun empty/command () | ||||
|   "Return the Clingon command for the \"empty\" subcommand." | ||||
|   (clingon:make-command | ||||
|    :name "empty" | ||||
|    :aliases '("erase" "rm") | ||||
|    :description "delete files from the trash permanently" | ||||
|    :options (empty/options) | ||||
|    :handler #'empty/handler)) | ||||
|  | ||||
|  | ||||
| ;; List trash directories command | ||||
| (defun list-trashes/handler (cmd) | ||||
|   "Handler for the \"list-trashes\" subcommand." | ||||
|   (let* ((only-home (clingon:getopt cmd :home)) | ||||
|          (only-toplevel (clingon:getopt cmd :toplevel)) | ||||
|          (null (clingon:getopt cmd :null)) | ||||
|          (dirs (cond | ||||
|                  ((or (and only-home only-toplevel) | ||||
|                       (not (or only-home only-toplevel))) | ||||
|                   (cl-xdg-trash:list-trash-directories)) | ||||
|                  (only-home | ||||
|                   (list (cl-xdg-trash:user-home-trash-directory))) | ||||
|                  (only-toplevel | ||||
|                   (cl-xdg-trash:list-toplevel-trash-directories))))) | ||||
|     (dolist (dir dirs) | ||||
|       (format t "~A" (uiop:native-namestring dir)) | ||||
|       (if null | ||||
|           (write-char #\Nul) | ||||
|           (terpri))))) | ||||
|  | ||||
| (defun list-trashes/options () | ||||
|   "Return options for the \"list-trashes\" subcommand." | ||||
|   (list | ||||
|    (clingon:make-option | ||||
|     :list | ||||
|     :key :ignored-trashes | ||||
|     :description "ignore the given trash directory" | ||||
|     :long-name "ignore-trash" | ||||
|     :persistent t))) | ||||
|     :flag | ||||
|     :key :home | ||||
|     :description "only list the user's home trash directory" | ||||
|     :short-name #\o | ||||
|     :long-name "home") | ||||
|    (clingon:make-option | ||||
|     :flag | ||||
|     :key :toplevel | ||||
|     :description "only list toplevel trash directories" | ||||
|     :short-name #\t | ||||
|     :long-name "toplevel") | ||||
|    (clingon:make-option | ||||
|     :flag | ||||
|     :key :null | ||||
|     :description "use \\0 (null byte) instead of newline" | ||||
|     :short-name #\0 | ||||
|     :long-name "null"))) | ||||
|  | ||||
| (defun list-trashes/command () | ||||
|   "Return the Clingon command for the \"list-trashes\" subcommand." | ||||
|   (clingon:make-command | ||||
|    :name "list-trashes" | ||||
|    :description "list known trash directories" | ||||
|    :options (list-trashes/options) | ||||
|    :handler #'list-trashes/handler)) | ||||
|  | ||||
|  | ||||
| ;; Size command | ||||
| (defun size/handler (cmd) | ||||
|   "Handler for the \"size\" subcommand." | ||||
|   ) | ||||
|  | ||||
| (defun size/options () | ||||
|   "Return options for the \"size\" subcommand." | ||||
|   (list | ||||
|    ())) | ||||
|  | ||||
| (defun size/command () | ||||
|   "Return the Clingon command for the \"size\" subcommand.") | ||||
|  | ||||
|  | ||||
|  | ||||
| ;; Toplevel command | ||||
| (defun toplevel/command () | ||||
|   "Return the toplevel command." | ||||
|   (clingon:make-command | ||||
| @ -660,11 +948,16 @@ The recognizes printf-style sequences are (parenthesis denote the mnemonic):~%") | ||||
|    :version "0.1.0" | ||||
|    :license "GPL3" | ||||
|    :authors '("Alexander Rosenberg <zanderpkg@pm.me>") | ||||
|    :options (toplevel/options) | ||||
|    :sub-commands (list (list/command) | ||||
|                        (put/command)) | ||||
|                        (put/command) | ||||
|                        (restore/command) | ||||
|                        (list-trashes/command) | ||||
|                        (empty/command)) | ||||
|    :handler #'(lambda (cmd) | ||||
|                 (clingon:print-usage-and-exit cmd t)))) | ||||
|                 (let ((args (clingon:command-arguments cmd))) | ||||
|                   (when args | ||||
|                     (error "Unknown subcommand: ~S" (car args))) | ||||
|                   (clingon:print-usage-and-exit cmd t))))) | ||||
|  | ||||
| (defparameter *toplevel/help-option* | ||||
|   (clingon:make-option | ||||
|  | ||||
		Reference in New Issue
	
	Block a user