diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-03 19:44:05 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-03 19:44:05 -0500 |
| commit | 59094e944b4a9e8c5e1a20724c2681ffe9b6155c (patch) | |
| tree | 0e4732e6e138684b37e1ba8efaef3e2285997441 /modules/dev-fkeys.el | |
| parent | d341beea020cfad52942b854a8f42d0be2e34ae9 (diff) | |
| download | dotemacs-59094e944b4a9e8c5e1a20724c2681ffe9b6155c.tar.gz dotemacs-59094e944b4a9e8c5e1a20724c2681ffe9b6155c.zip | |
fix: shell-quote F6 test-runner command arguments
`cj/--f6-test-runner-cmd-for` was building shell command strings with raw paths and stems via `format`. For ordinary names (`tests/test_foo.py`, `pkg/foo`) that worked fine. But a path with spaces or a stem with shell metacharacters would break or misbehave once the string hit `compile`. A Python test file under `dir with spaces/` would get tokenized as separate arguments.
I added `cj/--f6-shell-quote-argument` that escapes only when the argument doesn't match `cj/--f6-shell-safe-argument-regexp` (alphanumerics, slash, dot, dash, plus a small handful of safe punctuation). Ordinary paths skip the quoter and stay readable. Risky paths route through `shell-quote-argument`.
I wrapped the four interpolations in the test-runner builder: the elisp `FILE=` basename, the elisp `TEST=^test-stem-` regex, both pytest paths, and the Go `./rel-dir`. The Go branch also handles an empty rel-dir explicitly so the result stays `go test ./` instead of constructing `./` via format with an empty string.
I added three boundary tests: a Python path with spaces, an elisp stem with `;`, and a Go directory with spaces. Existing tests for ordinary paths continue to pass since the safe regex covers them.
Diffstat (limited to 'modules/dev-fkeys.el')
| -rw-r--r-- | modules/dev-fkeys.el | 32 |
1 files changed, 27 insertions, 5 deletions
diff --git a/modules/dev-fkeys.el b/modules/dev-fkeys.el index 8c5b388c..836b7cf6 100644 --- a/modules/dev-fkeys.el +++ b/modules/dev-fkeys.el @@ -320,6 +320,18 @@ languages fall back to the basename without extension." ;; ---------- F6 test-runner command builder ---------- +(defconst cj/--f6-shell-safe-argument-regexp "\\`[[:alnum:]_./=+@%:,^-]+\\'" + "Regexp matching shell arguments safe to interpolate unchanged.") + +(defun cj/--f6-shell-quote-argument (argument) + "Quote ARGUMENT for shell command interpolation when needed. +Simple file paths and test regexes are returned unchanged so existing +F6 command strings stay readable. Arguments containing whitespace or +shell-significant characters are escaped with `shell-quote-argument'." + (if (string-match-p cj/--f6-shell-safe-argument-regexp argument) + argument + (shell-quote-argument argument))) + (defun cj/--f6-test-runner-cmd-for (language is-test-file rel-path stem rel-dir) "Return shell command to run tests for the given primitives, or nil. LANGUAGE is the language symbol; IS-TEST-FILE is non-nil when the file @@ -337,14 +349,24 @@ TypeScript / JavaScript and unknown languages return nil." (if is-test-file ;; The project Makefile prepends `tests/' to FILE, so pass the ;; basename only — passing the rel-path produces `tests/tests/...'. - (format "make test-file FILE=%s" (file-name-nondirectory rel-path)) - (format "make test-name TEST=^test-%s-" stem))) + (format "make test-file FILE=%s" + (cj/--f6-shell-quote-argument + (file-name-nondirectory rel-path))) + (format "make test-name TEST=%s" + (cj/--f6-shell-quote-argument + (format "^test-%s-" stem))))) ('python (if is-test-file - (format "pytest %s" rel-path) - (format "pytest tests/test_%s.py" stem))) + (format "pytest %s" (cj/--f6-shell-quote-argument rel-path)) + (format "pytest %s" + (cj/--f6-shell-quote-argument + (format "tests/test_%s.py" stem))))) ('go - (format "go test ./%s" rel-dir)) + (format "go test %s" + (cj/--f6-shell-quote-argument + (if (string-empty-p rel-dir) + "./" + (format "./%s" rel-dir))))) (_ nil))) ;; ---------- F6 current-file orchestrator ---------- |
