diff --git a/stash.log b/stash.log index 9764738..c59f30c 100644 --- a/stash.log +++ b/stash.log @@ -6,3 +6,23 @@ Moved /home/glenn/.config/test to /home/glenn/.dotfiles/.config/test Symlink created at /home/glenn/.config/test pointing to /home/glenn/.dotfiles/.config/test [2024-09-22 11:08:52] Moved /home/glenn/.config/test to /home/glenn/.dotfiles/.config/test [2024-09-22 11:08:52] Symlink created at /home/glenn/.config/test pointing to /home/glenn/.dotfiles/.config/test +[2024-09-22 13:52:41] Skipped moving directory: /home/glenn/.dotfiles/.config/test +[2024-09-22 13:52:45] Operation cancelled by user. +[2024-09-22 14:00:22] Deleted directory: /home/glenn/.dotfiles/.config/test +[2024-09-22 14:00:22] Overwritten directory: /home/glenn/.dotfiles/.config/test +[2024-09-22 14:00:22] Symlink created at /home/glenn/.config/test pointing to /home/glenn/.dotfiles/.config/test +[2024-09-22-14-13-00] Moved /home/glenn/.config/test to /home/glenn/.dotfiles/.config/test +[2024-09-22-14-13-00] Symlink created at /home/glenn/.config/test pointing to /home/glenn/.dotfiles/.config/test +[2024-09-22-14-13-09] Backed up /home/glenn/.dotfiles/.config/test to /home/glenn/.dotfiles/.config/test.backup.2024-09-22-14-13-09 +[2024-09-22-14-13-09] Moved /home/glenn/.config/test to /home/glenn/.dotfiles/.config/test after backup +[2024-09-22-14-13-09] Symlink created at /home/glenn/.config/test pointing to /home/glenn/.dotfiles/.config/test +[2024-09-22-14-20-03] Moved /home/glenn/.config/test to /home/glenn/.dotfiles/.config/test +[2024-09-22-14-20-03] Symlink created at /home/glenn/.config/test pointing to /home/glenn/.dotfiles/.config/test +[2024-09-22-14-20-09] Backed up /home/glenn/.dotfiles/.config/test to /home/glenn/.dotfiles/.config/test.backup.2024-09-22-14-20-09 +[2024-09-22-14-45-22] Operation cancelled by user. +[2024-09-22 15:08:02] Moved /home/glenn/.config/test to /home/glenn/.dotfiles/.config/test +[2024-09-22 15:08:02] Symlink created at /home/glenn/.config/test pointing to /home/glenn/.dotfiles/.config/test +[2024-09-22-15-10-35] Operation cancelled by user. +[2024-09-22-15-11-04] Moved /home/glenn/.config/test to /home/glenn/.dotfiles/.config/test +[2024-09-22-15-11-04] Symlink created at /home/glenn/.config/test pointing to /home/glenn/.dotfiles/.config/test +[2024-09-22-15-11-19] Operation cancelled by user. diff --git a/stash.scm b/stash.scm index fe59c17..10166d2 100644 --- a/stash.scm +++ b/stash.scm @@ -1,21 +1,21 @@ ;;; stash.scm --- A Guile script for moving directories and creating symlinks with conflict resolution ;;; ;;; Author: Glenn Thompson -;;; Version: 1.1 +;;; Version: 1.2 ;;; Created: 2024-12-03 ;;; Compatibility: Guile 3.0.9 -;;; Keywords: symlink, file management, conflict resolution +;;; Keywords: symlink, file management, conflict resolution, backup ;;; ;;; Commentary: ;;; ;;; Stash is a command-line utility written in Guile Scheme designed to facilitate the movement of directories and ;;; creation of symbolic links (symlinks) for files and directories. It allows users to specify a source directory ;;; and a target directory where the symlink should be created. The utility handles potential conflicts with existing -;;; files or directories at the target location, offering users the choice to overwrite or skip the creation of new symlinks. +;;; files or directories at the target location, offering users the choice to overwrite, back up, or skip the creation of new symlinks. ;;; ;;; Main Features: ;;; - Command-line argument parsing for specifying source and target paths. -;;; - Conflict detection with interactive user resolution options (overwrite or skip). +;;; - Conflict detection with interactive user resolution options (overwrite, backup, skip, or cancel). ;;; - Moving directories and creating symlinks. ;;; - Simple and interactive user interface for easy use. ;;; @@ -31,27 +31,6 @@ ;;; This software is licensed under the MIT License. ;;; ;;; Code: -;; TODO: Refactor Repeated Logic: The logic for checking if a path is a file or directory and appending the basename is repeated in the create-symlink function. This could be encapsulatedin a helper function to improve readability and avoid redundancy. e.g. - -;; (define (build-target-path source target) -;; (string-append target "/" (basename source))) - -;; TODO: Modularize Conflict Handling: Separating conflict handling into its own module to keep the main functionality more focused. This will also help to extend the conflict resolution mechanism with more options (e.g., backup, replace). - -;; TODO: Add Logging Capabilities: It might be useful to log actions like creating symlinks, skipping conflicts, or canceling operations to a file for auditing purposes. Adding a logging function that appends these actions to a log file. e.g. - -;; (define (log-action message) -;; (with-output-to-file "guile-stash.log" -;; (lambda () (display message) (newline)) -;; #:append #t)) - -;; TODO: Improve Error Handling: While catch is used, it's broad and might not capture the exact error. Using more specific error types (like system-error and file-error) could improve the granularity of error reporting. Additionally, return appropriate exit codes for different failure conditions to support integration with other scripts. - -;; TODO: Optimize the delete-directory Function: Using rm -r is risky for recursive deletion in case of directory traversal attacks. You could implement a Scheme-native recursive delete function to avoid this. - -;; #TODO Addition of a Non-Interactive Mode: Adding a non-interactive mode that automatically skips or overwrites conflicts (based on a command-line flag) could make the script more flexible for use in automation workflows. e.g. - -;;; CODE ;; Import necessary modules (use-modules (ice-9 getopt-long) @@ -75,8 +54,8 @@ (define (current-timestamp) "Return the current date and time as a formatted string." (let* ((time (current-time)) - (seconds (time-second time))) ;; Extract the seconds from the time object - (strftime "%Y-%m-%d %H:%M:%S" (localtime seconds)))) + (seconds (time-second time))) + (strftime "%Y-%m-%d-%H-%M-%S" (localtime seconds)))) ;;; Improved logging function with timestamp (define (log-action message) @@ -93,6 +72,14 @@ (string-append (getenv "HOME") (substring path 1)) path)) +;;; Helper function to back up a directory or symlink +(define (backup-path path) + "Back up a directory or symlink by renaming it with a timestamp." + (let ((backup-path (string-append path ".backup." (current-timestamp)))) + (rename-file path backup-path) + (display (color-message (format #f "Backed up ~a to ~a\n" path backup-path) green-text)) + (log-action (format #f "Backed up ~a to ~a" path backup-path)))) + ;;; Function to handle command-line arguments (define (parse-arguments args) "Parse command-line arguments." @@ -101,6 +88,22 @@ (source (value #t) (required? #t)))))) opts)) +;;; Helper function to quote shell arguments +(define (shell-quote-argument arg) + "Quote shell argument to handle spaces or special characters." + (if (string-contains arg " ") + (string-append "\"" (string-replace arg "\"" "\\\"") "\"") + arg)) + +;;; Helper function to delete a directory recursively +(define (delete-directory path) + "Delete a directory recursively." + (if (file-is-directory? path) + (begin + (system (string-append "rm -rf " (shell-quote-argument path))) ;; Use rm -rf to delete recursively + (log-action (format #f "Deleted directory: ~a" path))) ;; Log the deletion + (display (color-message "Error: Path is not a directory.\n" red-text)))) + ;;; Helper function to move source to target (define (move-source-to-target source-dir target-dir) "Move the entire source directory to the target directory." @@ -108,10 +111,35 @@ (target-dir (expand-home target-dir)) (source-name (basename source-dir)) (target-source-dir (string-append target-dir "/" source-name))) - ;; Move the source directory into the target directory - (rename-file source-dir target-source-dir) - (display (color-message (format #f "Moved ~a to ~a\n" source-dir target-source-dir) green-text)) - (log-action (format #f "Moved ~a to ~a" source-dir target-source-dir)) + ;; Check if the target directory already exists + (if (file-exists? target-source-dir) + (begin + (display (color-message (format #f "Directory ~a already exists.\n" target-source-dir) yellow-text)) + ;; Handle the conflict: overwrite, backup, or skip + (let ((choice (prompt-user-for-backup-action))) + (cond + ((eq? choice 'overwrite) + (delete-directory target-source-dir) + (rename-file source-dir target-source-dir) + (display (color-message (format #f "Overwriting directory ~a.\n" target-source-dir) green-text)) + (log-action (format #f "Overwritten directory: ~a" target-source-dir))) + ((eq? choice 'backup) + (backup-path target-source-dir) + (rename-file source-dir target-source-dir) + (display (color-message (format #f "Moved ~a to ~a\n" source-dir target-source-dir) green-text)) + (log-action (format #f "Moved ~a to ~a after backup" source-dir target-source-dir))) + ((eq? choice 'skip) + (display (color-message "Skipping move operation.\n" green-text)) + (log-action (format #f "Skipped moving directory: ~a" target-source-dir))) + ((eq? choice 'cancel) + (display (color-message "Operation cancelled by user.\n" yellow-text)) + (log-action "Operation cancelled by user.") + (exit 0))))) + ;; If the target directory doesn't exist, proceed with the move + (begin + (rename-file source-dir target-source-dir) + (display (color-message (format #f "Moved ~a to ~a\n" source-dir target-source-dir) green-text)) + (log-action (format #f "Moved ~a to ~a" source-dir target-source-dir)))) target-source-dir)) ;; Return the path of the moved source directory ;;; Helper function to create a symlink in the parent directory of the original source @@ -129,35 +157,25 @@ (display (color-message (format #f "Symlink created at ~a pointing to ~a\n" symlink-target target-dir) green-text)) (log-action (format #f "Symlink created at ~a pointing to ~a" symlink-target target-dir)))))) -;;; Conflict resolution handler -(define (prompt-user-for-action) - "Prompt the user to decide how to handle a conflict: overwrite (o), skip (s), or cancel (c)." - (display (color-message "A conflict was detected. Choose action - Overwrite (o), Skip (s), or Cancel (c): " yellow-text)) +;;; Conflict resolution handler with backup option +(define (prompt-user-for-backup-action) + "Prompt the user to decide how to handle a conflict: overwrite (o), backup (b), skip (s), or cancel (c)." + (display (color-message "A conflict was detected. Choose action - Overwrite (o), Backup (b), Skip (s), or Cancel (c): " yellow-text)) (let ((response (read-line))) (cond ((string-ci=? response "o") 'overwrite) + ((string-ci=? response "b") 'backup) ((string-ci=? response "s") 'skip) ((string-ci=? response "c") 'cancel) (else (display (color-message "Invalid input. Please try again.\n" red-text)) - (prompt-user-for-action))))) + (prompt-user-for-backup-action))))) -;;; Function to check if a file is a symlink -(define (file-is-symlink? path) - "Check if a given file path is a symlink." - (let ((stat (lstat path))) - (eqv? (stat:type stat) 'symlink))) - -;;; Helper function to build the target symlink path -(define (build-target-path source target) - "Builds the symlink path for the source file or directory under the target directory." - (string-append target "/" (basename source))) - -;;; Function to handle conflicts +;;; Function to handle conflicts with symlinks and directories (define (handle-conflicts source target) "Handle conflicts when a symlink or file already exists at the target location." (display (color-message (format #f "Conflict detected! A file or symlink already exists at ~a.\n" target) yellow-text)) - (let ((choice (prompt-user-for-action))) ;; Prompt the user for action + (let ((choice (prompt-user-for-backup-action))) ;; Prompt for backup or overwrite (cond ((eq? choice 'overwrite) (if (file-is-symlink? target) @@ -165,20 +183,19 @@ (delete-file target) (display (color-message "Removed the existing symbolic link.\n" green-text)) (log-action (format #f "Removed the existing symbolic link at ~a" target)) - ;; Recreate the symlink after removing the old one (create-symlink-in-parent source target)) (display (color-message "Error: Target is not a symbolic link.\n" red-text)))) + ((eq? choice 'backup) + (backup-path target) ;; Backup the existing directory or symlink + (create-symlink-in-parent source target)) ;; After backup, create the new symlink ((eq? choice 'skip) - ;; Do nothing, log skipping the conflict (log-action (format #f "Skipped creating symlink at ~a" target)) (display (color-message "Keeping existing symbolic link.\n" green-text))) ((eq? choice 'cancel) - ;; Cancel the operation (log-action "Operation cancelled by user.") (display (color-message "Operation cancelled.\n" yellow-text)) (exit 0)) (else - ;; Invalid input (display (color-message "Invalid choice. Please try again.\n" red-text)) (handle-conflicts source target)))))