Recently Written · git

tmpl

Insanely simple templates

git clone https://github.com/equwal/tmpl

Log | Files | Refs


commit 64b7e8ef3a478450f80ed8581275f106c0a8ab85
map588 <map588@pitt.edu>
2026-09-27 02:18:04 -0400

Add tmpl CLI and generated INDEX.md

bin/tmpl lists templates with their summary comments, prints one by
path, or picks one with fzf. make index writes the list to INDEX.md.
The summary rule now requires whitespace after the comment marker,
so #include is not a summary. The make templates whose first line
was a syntax hint get a real summary.

 INDEX.md                              | 122 ++++++++++++++++++++++++++++++++++
 Makefile                              |   9 +++
 README.md                             |  16 +++++
 bin/tmpl                              |  51 ++++++++++++++
 convert_to_luasnip.py                 |   2 +-
 make/expansions/substitute-wordending |   1 +
 make/functions/call-fn                |   1 +
 make/functions/conditionals           |   1 +
 make/functions/file-fn                |   1 +
 make/functions/filenames              |   1 +
 make/functions/foreach-complex        |   1 +
 make/functions/text                   |   1 +
 make/origin-values                    |   1 +
 make/output/error                     |   1 +
 make/output/info                      |   1 +
 make/output/warning                   |   1 +
 test_convert_to_luasnip.py            |   3 +
 17 files changed, 213 insertions(+), 1 deletion(-)
diff --git a/INDEX.md b/INDEX.md
new file mode 100644
index 0000000..053c2ef
--- /dev/null
+++ b/INDEX.md
@@ -0,0 +1,122 @@
+# Index
+
+Generated by `make index`. Do not edit by hand.
+
+| Template | Summary |
+|---|---|
+| `c/main.c` | main with argc and argv |
+| `c/mqueues.c` |  |
+| `cl/block-return-from` | name, body, return from name |
+| `cl/class` | defclass with one slot |
+| `cl/defmethod-specialized` | defmethod specialized on two arguments |
+| `cl/defpackage` | defpackage with every common option |
+| `cl/defstruct` | defstruct with options and one slot |
+| `cl/defsystem` | ASDF defsystem with its own package |
+| `cl/do` | do or do* loop with two variables |
+| `cl/dolist` | dolist over a list |
+| `cl/dotimes` | dotimes counted loop |
+| `cl/handler-case` | handler-case with a :no-error clause |
+| `cl/hash-table-loop` | Hash table loop patterns |
+| `cl/lambda-list` | in this order |
+| `cl/mapping` |  |
+| `cl/plists-hint` |  |
+| `cl/quicklisp-in-file` | Yep! |
+| `cl/quickproject` | Create a new project with quickproject |
+| `cl/quickproject-make-project` |  |
+| `cl/read-time-eval` | Read-time evaluation - computed at read time, not runtime |
+| `cl/restart-case` | restart-case with a report and an interactive function |
+| `cl/slot` | defclass slot with every option |
+| `cl/string-adjustable-array` |  |
+| `cl/tagbody-go` | tagbody with go labels |
+| `cl/with-open-file` | with-open-file with direction and if-exists options |
+| `cl/with-simple-restart` | with-simple-restart around a body |
+| `epub.css` |  |
+| `git-config-branch` | git remote section for a GitHub repository |
+| `html/template.html` | HTML5 page skeleton |
+| `make/conditionals/conditional-variable` | Set a variable only when it is not set already |
+| `make/conditionals/ifeq-endif` | ifeq/ifneq conditional block |
+| `make/expansions/substitute-wordending` | Replace the word ending in every word of a variable |
+| `make/functions/call-fn` | call: expand a variable as a function with parameters |
+| `make/functions/conditionals` | if, or, and: conditional functions |
+| `make/functions/file-fn` | file: read or write a file |
+| `make/functions/filenames` | dir, notdir, suffix, basename: file name functions |
+| `make/functions/foreach-complex` | foreach: expand text once per list item |
+| `make/functions/foreach-simple` | foreach over a list (see foreach-complex for more) |
+| `make/functions/text` | subst, patsubst, strip, findstring: text functions |
+| `make/golf/if-pkgconfig-exists` | Use pkg-config for a library when it is installed (example: ncursesw) |
+| `make/macros/macros` |  |
+| `make/mapping/map-fn` | Define a map function from a one-argument function |
+| `make/mapping/origins` | see map-fn for the definition of map and use it |
+| `make/mapping/wildcards` | see map-fn and import that too |
+| `make/origin-values` | origin: the values it can return |
+| `make/output/error` | error: stop make with a message |
+| `make/output/info` | info: print a message |
+| `make/output/warning` | warning: print a message and continue |
+| `make/pedantic-phony` | Declare the standard phony targets |
+| `make/pedantic-posix` |  |
+| `make/pointer` | you use computed variable names as pointers |
+| `make/pointer-array` |  |
+| `make/rules/ordered-mkdir` | use ordered prereqs in a recipe to make a folder |
+| `make/rules/ordered-prereqs` | targets : normal-prereqs | order-only-prereqs [; recipe] |
+| `make/rules/var-globbed` | NOTE: You can do multiple globs too with clever mapping |
+| `make/shell` | Run a shell command and capture its output |
+| `make/templates/config.mk` | config.mk with version, shell and install prefixes |
+| `make/templates/dist` | Minimal makefile that builds a release tarball |
+| `make/templates/scripts` | include PROJECT=name in your config.mk |
+| `make/variables/globbing` | globbing not done in variable declarations |
+| `openrc.init` | OpenRC service script |
+| `sed/append-after-match` | Append a line after the first line that matches |
+| `sed/bin-sh` | Swap first line for #!/bin/sh naively |
+| `sed/d` | Delete matching lines with D (keeps the newline) or d |
+| `sed/TLDR` |  |
+| `sh/absolute-path` |  |
+| `sh/alias-function-args` | alias argsinmiddle='f(){ echo "$1" "$2" etc. }; f' |
+| `sh/charcount` | cheap hack |
+| `sh/dirname` | dirname clobbers multiple trailing newlines |
+| `sh/dotglob` | not: . .. |
+| `sh/echo-arg` | Echo, fixed so only the first argument can be flags. |
+| `sh/echo-noarg` | Portable echo. No arguments. |
+| `sh/epochsec` | date +%s not portable |
+| `sh/find-big-files` | GNU only: since du -h is a GNU extension |
+| `sh/find-exec-fn` | can't use -exec on shell functions, so do this |
+| `sh/find-print` |  |
+| `sh/find-xargs` | -print0/-0 is a GNU extension |
+| `sh/for-array` | Iterate over a saved argument list in POSIX sh |
+| `sh/for-find` | Yes, it really is this hard to do |
+| `sh/globber` | Same as Bash [[ ... ]] |
+| `sh/here-string` | BASHISM: |
+| `sh/isempty` |  |
+| `sh/join-lines` | Removes all newlines from the input stream, like holding J on line 1 in vi. |
+| `sh/newlocale` |  |
+| `sh/path` | Three methods here: |
+| `sh/pushd-popd-directory-switch` | LLM generated this genius thing for a Makefile uninstaller |
+| `sh/pwd` | fixup $PWD |
+| `sh/quote` | Safe quoting, safe trailing newlines. |
+| `sh/random` | 'gawk' can produce random numbers using srand().  In this example, 10 integers between 1 and 500: |
+| `sh/readbyte` | also see: writebyte |
+| `sh/readline` | Read into variables |
+| `sh/README` |  |
+| `sh/redirection` | Cursed shell redirection |
+| `sh/remove-duplicates` | sort the lines, then remove duplicates that are next to eachoter |
+| `sh/str-fn` | Shell function that returns a string by reference |
+| `sh/trap/background-jobs` | Clean up background jobs |
+| `sh/trap/cleanup-multiple` | Clean up multiple temporary files/directories |
+| `sh/trap/cleanup-temp` | Clean up temporary files on exit |
+| `sh/trap/debug-trace` | Enable debug tracing with cleanup |
+| `sh/trap/error-handler` | Exit on error with context |
+| `sh/trap/lock-file` | Exclusive lock using trap |
+| `sh/trap/progress-cleanup` | Show progress and clean up on interruption |
+| `sh/trap/restore-settings` | Save and restore shell settings |
+| `sh/trap/signal-handler` | Handle specific signals gracefully |
+| `sh/trap/timeout-handler` | Timeout handler |
+| `sh/tree-exec` | Alternative to find |
+| `sh/unexport` |  |
+| `sh/userhome` |  |
+| `sh/varprint` | Don't use echo, it isn't portable. |
+| `sh/whiletruessh` |  |
+| `sh/writebyte` | also see the other direction: readbyte |
+| `uad.json` |  |
+| `vim/sh-into-buffer` |  |
+| `vim/sh-quiet` |  |
+| `vim/use-g-v` |  |
+| `wpa_supplicant-verbose-example.conf` | Example wpa_supplicant configuration file ############################### |
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..56929bf
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,9 @@
+# Maintenance targets for the tmpl repository.
+
+.POSIX:
+.PHONY: index
+
+# Rebuild INDEX.md from the templates and their summary comments.
+index:
+	{ printf '# Index\n\nGenerated by `make index`. Do not edit by hand.\n\n| Template | Summary |\n|---|---|\n'; \
+	  bin/tmpl -l | awk -F '\t' '{ printf "| `%s` | %s |\n", $$1, $$2 }'; } > INDEX.md
diff --git a/README.md b/README.md
index 47f4fa4..ed33127 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,22 @@ Two kinds of file live here:
   for example `<+remote+>`. The snippet converter turns the name into the
   placeholder text. Use the plain form unless a name helps.
 
+# Finding a template
+
+`bin/tmpl` lists and prints templates. Put it on your `PATH` or call it by
+path.
+
+```sh
+tmpl -l                    # list every file with its summary
+tmpl -f                    # pick one with fzf and print it
+tmpl sh for-find           # print sh/for-find
+tmpl sh/trap/cleanup-temp  # the same, as one path
+```
+
+In Vim, `:r !tmpl sh for-find` reads a template in at the cursor.
+
+[INDEX.md](INDEX.md) is the same list as a table. `make index` regenerates it.
+
 # Using with Vim
 
 ```vim
diff --git a/bin/tmpl b/bin/tmpl
new file mode 100755
index 0000000..e683f49
--- /dev/null
+++ b/bin/tmpl
@@ -0,0 +1,51 @@
+#!/bin/sh
+# Print, list or pick templates from the tmpl repository.
+#
+# usage: tmpl -l            list templates as "path<TAB>summary"
+#        tmpl -f            pick a template with fzf and print it
+#        tmpl LANG NAME     print a template, for example: tmpl sh for-find
+#        tmpl PATH          print a template, for example: tmpl sh/trap/cleanup-temp
+#
+# TMPL_DIR overrides the template directory. The default is the parent of
+# the directory that holds this script.
+
+set -eu
+
+root=${TMPL_DIR:-$(cd "$(dirname "$0")/.." && pwd)}
+
+usage() {
+    sed -n '2,8p' "$0" | sed 's/^# \{0,1\}//' >&2
+    exit 2
+}
+
+# Print every template path relative to $root, one per line.
+list_paths() {
+    find "$root" -type f \
+        ! -path '*/.*' ! -path '*/__pycache__/*' \
+        ! -path "$root/bin/*" \
+        ! -name '*.py' ! -name '*.md' ! -name Makefile \
+        | sed "s|^$root/||" | sort
+}
+
+# Print the summary comment on the first line of a file. A #! line may come first.
+summary() {
+    awk 'NR == 1 && /^#!/ { next }
+         /^[ \t]*(#+|;+|\/\/|<!--)[ \t]+/ { sub(/^[ \t]*(#+|;+|\/\/|<!--)[ \t]+/, ""); sub(/[ \t]*-->[ \t]*$/, ""); print }
+         { exit }' "$1"
+}
+
+list() {
+    list_paths | while IFS= read -r path; do
+        printf '%s\t%s\n' "$path" "$(summary "$root/$path")"
+    done
+}
+
+case ${1:-} in
+    -l) list ;;
+    -f) pick=$(list | fzf --with-nth=1,2 --delimiter='\t' | cut -f1 | head -n 1)
+        [ -n "$pick" ] && cat "$root/$pick" ;;
+    -h|--help|'') usage ;;
+    *)  path=$(printf '%s/' "$@" | sed 's|/$||')
+        [ -f "$root/$path" ] || { printf 'tmpl: no template %s\n' "$path" >&2; exit 1; }
+        cat "$root/$path" ;;
+esac
diff --git a/convert_to_luasnip.py b/convert_to_luasnip.py
index 15f49bf..587a32c 100644
--- a/convert_to_luasnip.py
+++ b/convert_to_luasnip.py
@@ -34,7 +34,7 @@ ROOT_FILE_MAP = {
 PLACEHOLDER_RE = re.compile(r"<\+([^+>]*)\+>")
 
 # A summary comment on the first line of a template, in any supported comment syntax.
-HEAD_COMMENT_RE = re.compile(r"^\s*(?:#+|;+|//|<!--)\s*(.*?)\s*(?:-->)?\s*$")
+HEAD_COMMENT_RE = re.compile(r"^\s*(?:#+|;+|//|<!--)\s+(.*?)\s*(?:-->)?\s*$")
 
 
 def escape_snippet_text(text: str) -> str:
diff --git a/make/expansions/substitute-wordending b/make/expansions/substitute-wordending
index 1679a9b..ee6f591 100644
--- a/make/expansions/substitute-wordending
+++ b/make/expansions/substitute-wordending
@@ -1,3 +1,4 @@
+# Replace the word ending in every word of a variable
 # newvar := $(oldvar:old-end-of-word=new-end-of-word)
 # changes all the word endings
 # most common use is:
diff --git a/make/functions/call-fn b/make/functions/call-fn
index 888a75f..03514b2 100644
--- a/make/functions/call-fn
+++ b/make/functions/call-fn
@@ -1,3 +1,4 @@
+# call: expand a variable as a function with parameters
 #call VARIABLE,PARAM,PARAM,...
 #the variable contains a complex expression to be expanded
 #the params go into $(1), $(2),...etc. for expansion
diff --git a/make/functions/conditionals b/make/functions/conditionals
index 26bbb09..85de5b9 100644
--- a/make/functions/conditionals
+++ b/make/functions/conditionals
@@ -1,3 +1,4 @@
+# if, or, and: conditional functions
 #if CONDITION,THEN[,ELSE]
 #or CONDITION[,CONDITION[,..]..]
 #and CONDITION[,CONDITION[,..]..]
diff --git a/make/functions/file-fn b/make/functions/file-fn
index 51bd795..158bb95 100644
--- a/make/functions/file-fn
+++ b/make/functions/file-fn
@@ -1,3 +1,4 @@
+# file: read or write a file
 #file OP FILENAME[,TEXT]
 # OP is >, >>, or <
 # empty string on read failure
diff --git a/make/functions/filenames b/make/functions/filenames
index a28c84c..207e523 100644
--- a/make/functions/filenames
+++ b/make/functions/filenames
@@ -1,3 +1,4 @@
+# dir, notdir, suffix, basename: file name functions
 #dir LIST # directory parts and slash
 #notdir LIST # remove not directories from list AND do basename on the rest
 #suffix LIST # get file extensions (.whatever )AND remove items without
diff --git a/make/functions/foreach-complex b/make/functions/foreach-complex
index 9dd00eb..342c8b5 100644
--- a/make/functions/foreach-complex
+++ b/make/functions/foreach-complex
@@ -1,3 +1,4 @@
+# foreach: expand text once per list item
 #foreach VAR,LIST,TEXT
 # VAR and LIST are expanded
 # VAR is set to each item in LIST and TEXT is expanded repeatedly
diff --git a/make/functions/text b/make/functions/text
index fa659d3..c3cccac 100644
--- a/make/functions/text
+++ b/make/functions/text
@@ -1,3 +1,4 @@
+# subst, patsubst, strip, findstring: text functions
 #subst FROM,TO,TEXT # TEXT is the thing being operated against
 #patsubst PATTERN,REPLACEMENT,TEXT # pattern can be %.c for example
 #strip STRING # leading and trailing, good in conditionals
diff --git a/make/origin-values b/make/origin-values
index dec5be0..9772fd0 100644
--- a/make/origin-values
+++ b/make/origin-values
@@ -1,3 +1,4 @@
+# origin: the values it can return
 #$(origin VARIABLE)
 #also see the mapped version
 # undefined # a variation on empty
diff --git a/make/output/error b/make/output/error
index 5d3cc98..0e82e9f 100644
--- a/make/output/error
+++ b/make/output/error
@@ -1,3 +1,4 @@
+# error: stop make with a message
 #$(error TEXT...)
 #fatal error and print TEXT
 $(error <++>)
diff --git a/make/output/info b/make/output/info
index d0ef901..0553cd6 100644
--- a/make/output/info
+++ b/make/output/info
@@ -1,2 +1,3 @@
+# info: print a message
 #info TEXT...
 $(info <++>)
diff --git a/make/output/warning b/make/output/warning
index fb5a824..8d0ca5d 100644
--- a/make/output/warning
+++ b/make/output/warning
@@ -1,3 +1,4 @@
+# warning: print a message and continue
 #warning TEXT...
 #doesn't stop execution
 $(warning <++>)
diff --git a/test_convert_to_luasnip.py b/test_convert_to_luasnip.py
index 913ff7e..6a3eed5 100644
--- a/test_convert_to_luasnip.py
+++ b/test_convert_to_luasnip.py
@@ -124,3 +124,6 @@ def test_description_falls_back_to_path(tmp_path):
     f = root / "sh" / "bare"
     f.write_text("echo <++>\n")
     assert conv.get_description(f, root) == "sh/bare template"
+    g = root / "sh" / "cfile"
+    g.write_text("#include <stdio.h>\n<++>\n")
+    assert conv.get_description(g, root) == "sh/cfile template"