Building Racket CLI Applications
Quick Start
racket
1#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
racket
1(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
racket
1#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)
bash
1# 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:
racket
1#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:
bash
1raco pkg install --link .
2# Now 'my-cli' is available in PATH
Method 3: GraalVM Native Image (Advanced)
bash
1# Compile to bytecode
2raco make main.rkt
3# Use racket-native or wrap in GraalVM (experimental)
racket
1;; 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")))
Colored Output
racket
1(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
racket
1(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
racket
1;; 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
racket
1;; 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
racket
1(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
racket
1(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
racket
1#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
--help automatically via command-line
- Use exit codes consistently (0 = success)
- Write to stderr for errors and diagnostics
- Support
--version for installed tools
- Handle signals gracefully when possible
racket
1;; 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 ...)