Building Racket CLI Applications
Quick Start
racket1#lang racket 2(require racket/cmdline) 3 4(define verbose (make-parameter #f)) 5 6(command-line 7 #:program "my-cli" 8 #:once-each 9 [("-v" "--verbose") "Enable verbose output" (verbose #t)] 10 #:args (filename) 11 (when (verbose) (displayln "Processing...")) 12 (displayln filename))
Command-Line Parsing with racket/cmdline
Basic Flags and Arguments
racket1(require racket/cmdline) 2 3(define output-file (make-parameter "out.txt")) 4(define count (make-parameter 1)) 5 6(command-line 7 #:program "tool" 8 #:once-each 9 [("-o" "--output") file "Output file path" (output-file file)] 10 [("-n" "--count") n "Number of iterations" (count (string->number n))] 11 #:once-any 12 [("--json") "Output as JSON" (format-param 'json)] 13 [("--csv") "Output as CSV" (format-param 'csv)] 14 #:multi 15 [("-i" "--include") path "Include additional path" (includes (cons path (includes)))] 16 #:args (input-file . rest-files) 17 (process-files (cons input-file rest-files)))
Flag Types
| Directive | Purpose |
|---|---|
#:once-each | Flag can appear once |
#:once-any | Only one of these flags allowed |
#:multi | Flag can repeat, accumulates values |
#:final | Stops processing after this flag |
#:args | Positional arguments pattern |
#:usage-help | Custom usage message |
Subcommands Pattern
racket1#lang racket 2(require racket/cmdline) 3 4(define (cmd-init args) 5 (command-line #:program "mycli init" 6 #:argv args 7 #:args () (displayln "Initialized!"))) 8 9(define (cmd-run args) 10 (define watch (make-parameter #f)) 11 (command-line #:program "mycli run" 12 #:argv args 13 #:once-each [("-w" "--watch") "Watch mode" (watch #t)] 14 #:args (file) (run-file file (watch)))) 15 16(define (main) 17 (define args (current-command-line-arguments)) 18 (when (zero? (vector-length args)) 19 (displayln "Usage: mycli <command> [options]") 20 (displayln "Commands: init, run") 21 (exit 1)) 22 (match (vector-ref args 0) 23 ["init" (cmd-init (vector-drop args 1))] 24 ["run" (cmd-run (vector-drop args 1))] 25 [cmd (eprintf "Unknown command: ~a~n" cmd) (exit 1)])) 26 27(module+ main (main))
Packaging as Executable
Method 1: raco exe (Standalone Binary)
bash1# Create standalone executable 2raco exe -o my-cli main.rkt 3 4# Create distribution with dependencies 5raco distribute dist-folder my-cli
Method 2: Launcher via info.rkt (Installed with Package)
In info.rkt:
racket1#lang info 2(define collection "my-package") 3(define deps '("base")) 4 5;; Define CLI launchers 6(define racket-launcher-names '("my-cli" "my-cli-admin")) 7(define racket-launcher-libraries '("main.rkt" "admin.rkt"))
Install with:
bash1raco pkg install --link . 2# Now 'my-cli' is available in PATH
Method 3: GraalVM Native Image (Advanced)
bash1# Compile to bytecode 2raco make main.rkt 3# Use racket-native or wrap in GraalVM (experimental)
Interactive Input
racket1;; Simple prompt 2(define (prompt msg) 3 (display msg) 4 (flush-output) 5 (read-line)) 6 7;; Password input (no echo - requires terminal) 8(define (prompt-password msg) 9 (display msg) 10 (flush-output) 11 (system "stty -echo") 12 (define pw (read-line)) 13 (system "stty echo") 14 (newline) 15 pw) 16 17;; Confirmation 18(define (confirm? msg) 19 (define response (prompt (format "~a [y/N]: " msg))) 20 (member (string-downcase (string-trim response)) '("y" "yes")))
Output Formatting
Colored Output
racket1(require racket/format) 2 3(define (color code text) 4 (format "\033[~am~a\033[0m" code text)) 5 6(define (red text) (color 31 text)) 7(define (green text) (color 32 text)) 8(define (yellow text) (color 33 text)) 9(define (blue text) (color 34 text)) 10(define (bold text) (color 1 text)) 11 12(displayln (green "✓ Success")) 13(displayln (red "✗ Error"))
Progress Indicators
racket1(define (with-spinner msg thunk) 2 (define frames '("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")) 3 (define done? (box #f)) 4 (define spinner-thread 5 (thread 6 (λ () 7 (let loop ([i 0]) 8 (unless (unbox done?) 9 (printf "\r~a ~a" (list-ref frames (modulo i 10)) msg) 10 (flush-output) 11 (sleep 0.1) 12 (loop (add1 i))))))) 13 (define result (thunk)) 14 (set-box! done? #t) 15 (thread-wait spinner-thread) 16 (printf "\r✓ ~a~n" msg) 17 result)
Exit Codes
racket1;; Standard exit codes 2(define EXIT-SUCCESS 0) 3(define EXIT-ERROR 1) 4(define EXIT-USAGE 64) ; EX_USAGE from sysexits.h 5(define EXIT-DATAERR 65) ; EX_DATAERR 6(define EXIT-NOINPUT 66) ; EX_NOINPUT 7 8(define (die! msg [code EXIT-ERROR]) 9 (eprintf "Error: ~a~n" msg) 10 (exit code)) 11 12;; Usage 13(unless (file-exists? input-file) 14 (die! (format "File not found: ~a" input-file) EXIT-NOINPUT))
Environment Variables
racket1;; Reading 2(define api-key (getenv "API_KEY")) 3(define debug? (equal? (getenv "DEBUG") "1")) 4(define home (or (getenv "HOME") (find-system-path 'home-dir))) 5 6;; With defaults 7(define port (string->number (or (getenv "PORT") "8080"))) 8 9;; Setting (for child processes) 10(putenv "MY_VAR" "value")
Configuration Files
XDG-Compliant Config Location
racket1(define (config-dir) 2 (or (getenv "XDG_CONFIG_HOME") 3 (build-path (find-system-path 'home-dir) ".config"))) 4 5(define (app-config-path app-name) 6 (build-path (config-dir) app-name "config.toml"))
Simple Config Loading
racket1(require json) 2 3(define (load-config path) 4 (if (file-exists? path) 5 (with-input-from-file path read-json) 6 (hash))) 7 8(define (save-config path data) 9 (make-parent-directory* path) 10 (with-output-to-file path 11 #:exists 'replace 12 (λ () (write-json data))))
Testing CLIs
racket1#lang racket 2(require rackunit) 3 4;; Capture stdout/stderr 5(define (capture-output thunk) 6 (define out (open-output-string)) 7 (define err (open-output-string)) 8 (parameterize ([current-output-port out] 9 [current-error-port err]) 10 (thunk)) 11 (values (get-output-string out) 12 (get-output-string err))) 13 14;; Test CLI with args 15(define (run-cli-test args) 16 (parameterize ([current-command-line-arguments (list->vector args)]) 17 (capture-output main))) 18 19(test-case "help flag shows usage" 20 (define-values (out err) (run-cli-test '("--help"))) 21 (check-regexp-match #rx"Usage:" out))
Best Practices
- Use parameters for configuration, not global variables
- Validate inputs early with clear error messages
- Support
--helpautomatically via command-line - Use exit codes consistently (0 = success)
- Write to stderr for errors and diagnostics
- Support
--versionfor installed tools - Handle signals gracefully when possible
racket1;; Version flag 2(command-line 3 #:program "my-cli" 4 #:once-each 5 [("--version") "Show version" 6 (displayln "my-cli v1.0.0") (exit 0)] 7 ...)