Skip to content

Instantly share code, notes, and snippets.

@david-bakin
Last active September 11, 2026 23:22
Show Gist options
  • Select an option

  • Save david-bakin/e373c69d6405c1c65733de1965cf9ca7 to your computer and use it in GitHub Desktop.

Select an option

Save david-bakin/e373c69d6405c1c65733de1965cf9ca7 to your computer and use it in GitHub Desktop.
For the Racket language: Use a `scribble/text` module as a text-template system
  • scribble-arg-processing.rkt: require this in your scribble/text module to get access to the arguments you're passing in to be used in the template - which you can access by index or as a key/value store,
  • generate-string-from-scribble.rkt: this has the function generate-string-from-scribble-module where you specify your scribble module's path and the arguments you want it to have access to and it returns the processed text template as a string,
  • command-line-serialization.rkt: helper module
  • template-test.c.indexed.scrbl: a test template (a C program that will takes 3 arguments, in a sequence, to be accessed by index)
  • template-test.c.dictionary.scrbl: a test template (same C program as above, but this time accesses the arguments by key)
  • template-test.rkt: test driver to expand the previous two templates
#lang racket/base
;; For marshalling things through the command-line argument interface. That interface, via parameter
;; `current-command-line-arguments`, wants a vector of strings. So we have a pair of serdes routines
;; that'll round-trip (nearly) anything by transforming it first into a singleton vector holding the
;; `write` version of the datum, and then back via `read`.
(require racket/contract)
(provide
(contract-out
;; serialize
[any->vstring (-> any/c (and/c (vectorof string?) singleton? immutable?))]
;; deserialize
[vstring->any (-> (and/c (vectorof string?) singleton?) any/c)]))
;; --------------------------------------------------------------------------------------------------
;; implementation
(require racket/port)
;; --------------------------------------------------------------------------------------------------
;; private helpers
(define (singleton? vec)
(= 1 (vector-length vec)))
;; --------------------------------------------------------------------------------------------------
;; provided (exported) functions
;; serialize anything to a length-1 vector holding a string (that can be round-tripped)
(define/contract (any->vstring arg)
(-> any/c (and/c (vectorof string?) singleton? immutable?))
(let ((s (with-output-to-string (λ () (write arg)))))
(vector->immutable-vector (make-vector 1 s))))
;; deserialize from a length-1 vector holding a string to anything
(define/contract (vstring->any str)
(-> (and/c (vectorof string?) singleton?) any/c)
(with-input-from-string (vector-ref str 0) read))
(module+ test
(require rackunit)
(check-equal? (any->vstring '()) #("()"))
(check-equal? (vstring->any (any->vstring '())) '())
(check-equal? (any->vstring #hash(("abc" . "x123") ("def" . "y321")))
#("#hash((\"abc\" . \"x123\") (\"def\" . \"y321\"))"))
(check-equal? (vstring->any (any->vstring #hash(("abc" . "x123") ("def" . "y321"))))
#hash(("abc" . "x123") ("def" . "y321"))))
#lang racket/base
;; Use a scribble/text module as a text template expander. You can pass it arguments that can then
;; be used from within the module: Those arguments can be either a sequence of datums that can be
;; referenced by (0-based) index, or they can be in a hash-table (key/value store).
;; Identify the scribble/text module you want to use by some sort of module path.
;; Works with scribble-arg-processing.rkt.
(require racket/contract)
(provide
(contract-out
[generate-string-from-scribble-module
(->* [(or/c module-path? resolved-module-path?)] [(or/c sequence? hash?)] string?)]))
;; --------------------------------------------------------------------------------------------------
;; implementation
(require "command-line-serialization.rkt")
(require racket/port)
;; --------------------------------------------------------------------------------------------------
;; private helpers
(define-namespace-anchor ns-anchor)
;; Run a function in a new namespace that has racket/base available (since this is a new namespace
;; you can load modules in it that are already loaded in your running program (namespace) and they'll
;; get reinitialized and everything. The new namespace lasts only for the duration of this call (of
;; course).
(define/contract (with-local-namespace proc)
(-> (-> any) any)
(let ((temp-ns (make-base-namespace)) ; racket/base attached and `require`d
(src-ns (namespace-anchor->namespace ns-anchor))) ; namespace of _this_ module
(parameterize ((current-namespace temp-ns)) ; now move to new racket/base namespace
(proc))))
;; --------------------------------------------------------------------------------------------------
;; provided (exported) functions
(define (generate-string-from-scribble-module module-path [command-line-args #()])
(let ((serialized-args (any->vstring command-line-args)))
(with-local-namespace
(λ ()
(with-output-to-string
(λ ()
(parameterize ([current-command-line-arguments serialized-args])
(dynamic-require module-path #f)
)))))))
;; --------------------------------------------------------------------------------------------------
;; Massive help from Philip McGrath (via Discord) to do the most difficult and Racket-specific part:
#;(require racket/runtime-path)
#;(define-runtime-module-path-index mpi "thats-c-baby-c-main-template.scrbl")
#;(define/contract (generate-string [args #("a" "b" "c")])
(->* [] [(vectorof string?)] string?)
(with-output-to-string
(λ ()
(parameterize ([current-command-line-arguments args])
(dynamic-require mpi #f)
))))
;; Yet more massive help from ThePuzzlemaker's project "weave", see
;; https://codeberg.org/ThePuzzlemaker/weave/src/branch/main/private/weave.rkt
;; for how to create a namespace and attach&require modules into it (though I'm not using that
;; latter feature here).
#lang racket/base
;; In a scribble module provide a nice syntax for fetching arguments from the command line:
;; a) by index - use `@arg-n[2]` (0-based of course)
;; b) by name (assuming arguments are passed as name/value pairs) - use `@arg-kv["keyword"]`
;; Also a capability to _dump_ the command line arguments so you can see what you're doing.
;; Start each document with:
;; ```
;; #lang scribble/text
;; @require["scribble-arg-processing.rkt"]
;; ...
;; ```
;; (Of course all normal scribble/text facilities are still available for your template.)
;; Works with generate-string-from-scribble.rkt.
(require racket/contract)
(provide
(contract-out
;; access argument by key name (treating arguments as a dictionary/hash-table)
[arg-kv (-> string? any)]
;; access argument by (0-based) index
[arg-n (-> exact-nonnegative-integer? any)]
;; dump arguments into document (debugging, typically)
[arg-dump-raw (-> void?)]
[arg-dump-actual (-> void?)]
))
;; --------------------------------------------------------------------------------------------------
;; implementation
(require "command-line-serialization.rkt")
(require racket/sequence)
;; --------------------------------------------------------------------------------------------------
;; private helpers
(define raw-args (current-command-line-arguments))
(define actual-args 'uncached)
(define (get-actual-args)
(if (eq? 'uncached actual-args)
(set! actual-args (vstring->any raw-args))
#t)
actual-args)
;; --------------------------------------------------------------------------------------------------
;; provided (exported) functions
;; Returns the nth actual argument (0-based)
(define (arg-n n)
(*arg-n n (get-actual-args)))
(define/contract (*arg-n n args)
(-> exact-nonnegative-integer? (or/c sequence? hash?) any/c)
(cond ((not (sequence? args))
(error (format "arg-n: actual arguments are not a sequence (have arguments: ~a)" args)))
((>= n (sequence-length args))
(error (format "arg index to high: ~a (have ~a arguments: ~a)"
n
(sequence-length args)
args)))
(#t (sequence-ref args n))))
(module+ test
(require rackunit)
(check-exn exn:fail? (λ () (*arg-n 2 #hash(("abc"."def")))))
(check-exn exn:fail? (λ () (*arg-n 2 #("abc" "def"))))
(check-equal? "abc" (*arg-n 0 #("abc" "def")))
(check-equal? "def" (*arg-n 1 '("abc" "def"))))
;; Returns the value accessed by a key, when treating the actual args as a dictionary (hash table)
(define (arg-kv key)
(*arg-kv key (get-actual-args)))
(define/contract (*arg-kv key args)
(-> any/c (or/c sequence? hash?) any/c)
(cond ((not (hash? args))
(error (format "arg-kv: actual arguments are not a hash table (have arguments: ~a)" args)))
(#t (hash-ref args
key
(λ () (error
(format "arg-kv: key not found: ~a (have ~a k/v arguments: ~a)"
key
(hash-count args)
args)))))))
(module+ test
(check-exn exn:fail? (λ () (*arg-kv "abc" "abcdef")))
(check-exn exn:fail? (λ () (*arg-kv "ghi" #hash(("aaa"."foo") ("bbb"."bar")))))
(check-equal? (*arg-kv "aaa" #hash(("aaa"."foo") ("bbb"."bar"))) "foo")
(check-equal? (*arg-kv "bbb" #hash(("aaa"."foo") ("bbb"."bar"))) "bar"))
;; dump into the document the raw arguments straight from the environment
(define (arg-dump-raw)
(*arg-dump-raw (current-command-line-arguments)))
(define/contract (*arg-dump-raw args)
(-> any/c void?)
(printf "arg-dump-raw: 『~a』\n" args))
(define (arg-dump-actual)
(*arg-dump-actual (get-actual-args)))
(define (*arg-dump-actual args)
(printf "arg-dump-actual: 『~a』\n" args))
;; --------------------------------------------------------------------------------------------------
#lang scribble/text
@require["scribble-arg-processing.rkt"]
/*
* "can return all integer values from 0 to 767 depending ..."
*/
unsigned return_value(void) {
unsigned m = 0;
m += (((char)-1) < 0) ? 1 : 0;
m += (s.f < 0) ? 2 : 0;
m += (sizeof(S) < sizeof(L)) ? 4 : 0;
#ifndef @arg-kv["macro-defn"]
m += @arg-kv["incr-value"];
m += (sizeof("??-") != 4) ? @arg-kv["incr-value"] : 0;
#endif
return m;
}
/*
* @arg-kv["comment-hdr"]:
* Write a C function that returns an integer to the caller, with the
* following constraints:
*/
#lang scribble/text
@require["scribble-arg-processing.rkt"]
/*
* "can return all integer values from 0 to 767 depending ..."
*/
unsigned return_value(void) {
unsigned m = 0;
m += (((char)-1) < 0) ? 1 : 0;
m += (s.f < 0) ? 2 : 0;
m += (sizeof(S) < sizeof(L)) ? 4 : 0;
#ifndef @arg-n[0]
m += @arg-n[1];
m += (sizeof("??-") != 4) ? @arg-n[1] : 0;
#endif
return m;
}
/*
* @arg-n[2]:
* Write a C function that returns an integer to the caller, with the
* following constraints:
*/
#lang racket
(require "generate-string-from-scribble.rkt")
(define test-template-indexed-module (build-path (current-directory) "template-test.c.indexed.scrbl"))
(define test-template-dictionary-module (build-path (current-directory) "template-test.c.dictionary.scrbl"))
(define/contract (expand-test-template-indexed arguments)
(-> (or/c sequence? hash?) string?)
(generate-string-from-scribble-module
test-template-indexed-module
arguments))
(define/contract (expand-test-template-dictionary arguments)
(-> (or/c sequence? hash?) string?)
(generate-string-from-scribble-module
test-template-dictionary-module
arguments))
(module+ test
(require rackunit)
(check-equal? (expand-test-template-indexed '("__STRICT_ANSI__" "1234321" "TODO follows"))
(expand-test-template-dictionary
#hash(("macro-defn" . "__STRICT_ANSI__")
("comment-hdr" . "TODO follows")
("incr-value" . "1234321")))))
(define/contract (print-expanded-test-templates)
(-> void?)
(display (expand-test-template-indexed '("__STRICT_ANSI__" "1234321" "TODO follows")))
(display "//////////////////////////////////////////////////////////////////////////////////////")
(display (expand-test-template-dictionary
#hash(("macro-defn" . "__STRICT_ANSI__")
("comment-hdr" . "TODO follows")
("incr-value" . "1234321")))))
@david-bakin

Copy link
Copy Markdown
Author

Hmm. I seem to have forgotten to add any license information anywhere here. Well, consider it all SPDX MIT-0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment