# Replacing Bash with Guile Scheme for Home Lab Tools This document outlines a proposal to migrate the `home-lab-tools` script from Bash to GNU Guile Scheme. This change aims to address the increasing complexity of the script and leverage the benefits of a more powerful programming language. ## 1. Introduction: Why Guile Scheme? GNU Guile is the official extension language for the GNU Project. It is an implementation of the Scheme programming language, a dialect of Lisp. Using Guile for scripting offers several advantages over Bash, especially as scripts grow in size and complexity. Key reasons for considering Guile: * **Expressiveness and Power:** Scheme is a full-fledged programming language with features like first-class functions, macros, and a rich standard library. This allows for more elegant and maintainable solutions to complex problems. * **Better Error Handling:** Guile provides robust error handling mechanisms (conditions and handlers) that are more sophisticated than Bash's `set -e` and trap. * **Modularity:** Guile supports modules, making it easier to organize code into reusable components. * **Data Manipulation:** Scheme excels at handling structured data, which can be beneficial for managing configurations or parsing output from commands. * **Readability (for Lisp programmers):** While Lisp syntax can be initially unfamiliar, it can lead to very clear and concise code once learned. * **Interoperability:** Guile can easily call external programs and libraries, and can be extended with C code if needed. ## 2. Advantages over Bash for `home-lab-tools` Migrating `home-lab-tools` from Bash to Guile offers specific benefits: * **Improved Logic Handling:** Complex conditional logic, loops, and function definitions are more naturally expressed in Guile. The current Bash script uses case statements and string comparisons extensively, which can become unwieldy. * **Structured Data Management:** Machine definitions, deployment modes, and status information could be represented as Scheme data structures (lists, association lists, records), making them easier to manage and query. * **Enhanced Error Reporting:** More descriptive error messages and better control over script termination in case of failures. * **Code Reusability:** Functions for common tasks (e.g., SSHing to a machine, running `nixos-rebuild`) can be more cleanly defined and reused. * **Easier Testing:** Guile's nature as a programming language makes it more amenable to unit testing individual functions or modules. * **Future Extensibility:** Adding new commands, machines, or features will be simpler and less error-prone in a more structured language. ## 3. Setting up Guile Guile is often available through system package managers. On NixOS, it can be added to your environment or system configuration. ```nix # Example: Adding Guile to a Nix shell nix-shell -p guile ``` A Guile script typically starts with a shebang line: ```scheme #!/usr/bin/env guile !# ``` The `!#` at the end is a Guile-specific convention that allows the script to be both executable and loadable into a Guile REPL. ## 4. Basic Guile Scripting Concepts * **S-expressions:** Code is written using S-expressions (Symbolic Expressions), which are lists enclosed in parentheses, e.g., `(function arg1 arg2)`. * **Definitions:** `(define variable value)` and `(define (function-name arg1 arg2) ...body...)`. * **Procedures (Functions):** Core of Guile programming. * **Control Flow:** `(if condition then-expr else-expr)`, `(cond (test1 expr1) (test2 expr2) ... (else else-expr))`, `(case ...)` * **Modules:** `(use-modules (ice-9 popen))` for using libraries. ## 5. Interacting with the System Guile provides modules for system interaction: * **(ice-9 popen):** For running external commands and capturing their output (similar to backticks or `$(...)` in Bash). * `open-pipe* command mode`: Opens a pipe to a command. * `get-string-all port`: Reads all output from a port. * **(ice-9 rdelim):** For reading lines from ports. * **(ice-9 filesys):** For file system operations (checking existence, deleting, etc.). * `file-exists? path` * `delete-file path` * **(srfi srfi-1):** List processing utilities. * **(srfi srfi-26):** `cut` for partial application, useful for creating specialized functions. * **Environment Variables:** `(getenv "VAR_NAME")`, `(setenv "VAR_NAME" "value")`. ## Example: Running a command** ```scheme (use-modules (ice-9 popen)) (define (run-command . args) (let* ((cmd (string-join args " ")) (port (open-pipe* cmd OPEN_READ))) (let ((output (get-string-all port))) (close-pipe port) output))) (display (run-command "echo" "Hello from Guile")) (newline) ``` ## 6. Error Handling Guile uses a condition system for error handling. * `catch`: Allows you to catch specific types of errors. * `throw`: Raises an error. ```scheme (use-modules (ice-9 exceptions)) (catch #t (lambda () (display "Trying something that might fail... ") ;; Example: Force an error (if #t (error "Something went wrong!")) (display "This won't be printed if an error occurs above. ")) (lambda (key . args) (format (current-error-port) "Caught an error: ~a - Args: ~a " key args) #f)) ; Return value indicating an error was caught ``` For `home-lab-tools`, this means we can provide more specific feedback when a deployment fails or a machine is unreachable. ## 7. Modularity and Code Organization Guile's module system allows splitting the code into logical units. For `home-lab-tools`, we could have modules for: * `lab-config`: Machine definitions, paths. * `lab-deploy`: Functions related to deploying configurations. * `lab-ssh`: SSH interaction utilities. * `lab-status`: Functions for checking machine status. * `lab-utils`: General helper functions, logging. **Example module structure:** ```scheme ;; file: lab-utils.scm (define-module (lab utils) #:export (log success warn error)) (define blue "") (define nc "") (define (log msg) (format #t "~a[lab]~a ~a " blue nc msg)) ;; ... other logging functions ``` ```scheme ;; file: main-lab-script.scm #!/usr/bin/env guile !# (use-modules (lab utils) (ice-9 popen)) (log "Starting lab script...") ;; ... rest of the script ``` ## 8. Example: Rewriting a Small Part of `home-lab-tools.nix` (Conceptual) Let's consider the `log` function and a simplified `deploy_machine` for local deployment. **Current Bash:** ```bash BLUE='' NC='' # No Color log() { echo -e "''${BLUE}[lab]''${NC} $1" } deploy_machine() { local machine="$1" # ... if [[ "$machine" == "congenital-optimist" ]]; then log "Deploying $machine (mode: $mode) locally" sudo nixos-rebuild $mode --flake "$HOMELAB_ROOT#$machine" fi # ... } ``` **Conceptual Guile Scheme:** ```scheme ;; main-lab-script.scm #!/usr/bin/env guile !# (use-modules (ice-9 popen) (ice-9 rdelim) (ice-9 pretty-print) (ice-9 exceptions) (srfi srfi-1)) ;; For list utilities like `string-join` ;; Configuration (could be in a separate module) (define homelab-root "/home/geir/Home-lab") ;; Color Definitions (define RED "") (define GREEN "") (define YELLOW "") (define BLUE "") (define NC "") ;; Logging functions (define (log level-color level-name message) (format #t "~a[~a]~a ~a " level-color level-name NC message)) (define (info . messages) (log BLUE "lab" (apply string-append (map (lambda (m) (if (string? m) m (format #f "~s" m))) messages)))) (define (success . messages) (log GREEN "lab" (apply string-append (map (lambda (m) (if (string? m) m (format #f "~s" m))) messages)))) (define (warn . messages) (log YELLOW "lab" (apply string-append (map (lambda (m) (if (string? m) m (format #f "~s" m))) messages)))) (define (err . messages) (log RED "lab" (apply string-append (map (lambda (m) (if (string? m) m (format #f "~s" m))) messages))) (exit 1)) ;; Exit on error ;; Function to run shell commands and handle output/errors (define (run-shell-command . command-parts) (let ((command-string (string-join command-parts " "))) (info "Executing: " command-string) (let ((pipe (open-pipe* command-string OPEN_READ))) (let loop ((lines '())) (let ((line (read-line pipe))) (if (eof-object? line) (begin (close-pipe pipe) (reverse lines)) ;; Return lines in order (begin (display line) (newline) ;; Display live output (loop (cons line lines))))))) ;; TODO: Add proper error checking based on exit status of the command ;; For now, we assume success if open-pipe* doesn't fail. ;; A more robust solution would check `close-pipe` status or use `system*`. )) ;; Simplified deploy_machine (define (deploy-machine machine mode) (info "Deploying " machine " (mode: " mode ")") (cond ((string=? machine "congenital-optimist") (info "Deploying " machine " locally") (catch #t (lambda () (run-shell-command "sudo" "nixos-rebuild" mode "--flake" (string-append homelab-root "#" machine)) (success "Successfully deployed " machine)) (lambda (key . args) (err "Failed to deploy " machine ". Error: " key " Args: " args)))) ;; Add other machines here (else (err "Unknown machine: " machine)))) ;; Main script logic (parsing arguments, calling functions) (define (main args) (if (< (length args) 3) (begin (err "Usage: