diff options
| -rw-r--r-- | modules/dev-fkeys.el | 23 | ||||
| -rw-r--r-- | modules/system-lib.el | 18 | ||||
| -rw-r--r-- | tests/test-system-lib-shell-quote-argument-readable.el | 55 |
3 files changed, 79 insertions, 17 deletions
diff --git a/modules/dev-fkeys.el b/modules/dev-fkeys.el index c9a5fc13..170e70b9 100644 --- a/modules/dev-fkeys.el +++ b/modules/dev-fkeys.el @@ -41,6 +41,7 @@ ;;; Code: (require 'cl-lib) +(require 'system-lib) (declare-function projectile-compile-project "projectile" (arg)) (declare-function projectile-run-project "projectile" (arg)) @@ -334,18 +335,6 @@ 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 @@ -364,20 +353,20 @@ TypeScript / JavaScript and unknown languages return nil." ;; 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" - (cj/--f6-shell-quote-argument + (cj/shell-quote-argument-readable (file-name-nondirectory rel-path))) (format "make test-name TEST=%s" - (cj/--f6-shell-quote-argument + (cj/shell-quote-argument-readable (format "^test-%s-" stem))))) ('python (if is-test-file - (format "pytest %s" (cj/--f6-shell-quote-argument rel-path)) + (format "pytest %s" (cj/shell-quote-argument-readable rel-path)) (format "pytest %s" - (cj/--f6-shell-quote-argument + (cj/shell-quote-argument-readable (format "tests/test_%s.py" stem))))) ('go (format "go test %s" - (cj/--f6-shell-quote-argument + (cj/shell-quote-argument-readable (if (string-empty-p rel-dir) "./" (format "./%s" rel-dir))))) diff --git a/modules/system-lib.el b/modules/system-lib.el index f932353f..dc1f8316 100644 --- a/modules/system-lib.el +++ b/modules/system-lib.el @@ -36,6 +36,24 @@ keep working." :warning) nil))) +(defconst cj/shell-safe-argument-regexp "\\`[[:alnum:]_./=+@%:,^-]+\\'" + "Regexp matching shell arguments safe to interpolate unchanged. +Members of this character set survive shell parsing without quoting, +so a command line containing only these characters in each argument +remains both safe and readable.") + +(defun cj/shell-quote-argument-readable (argument) + "Quote ARGUMENT for shell command interpolation when needed. + +When ARGUMENT consists only of characters in `cj/shell-safe-argument-regexp' +it is returned unchanged so the surrounding command stays human-readable +(useful for compile/test command lines you'll inspect in *compilation*). +Otherwise falls back to `shell-quote-argument' so the result is safe to +interpolate." + (if (string-match-p cj/shell-safe-argument-regexp argument) + argument + (shell-quote-argument argument))) + (defun cj/log-silently (format-string &rest args) "Append formatted message (FORMAT-STRING with ARGS) to *Messages* buffer. This does so without echoing in the minibuffer." diff --git a/tests/test-system-lib-shell-quote-argument-readable.el b/tests/test-system-lib-shell-quote-argument-readable.el new file mode 100644 index 00000000..1a0c7227 --- /dev/null +++ b/tests/test-system-lib-shell-quote-argument-readable.el @@ -0,0 +1,55 @@ +;;; test-system-lib-shell-quote-argument-readable.el --- Tests for cj/shell-quote-argument-readable -*- lexical-binding: t; -*- + +;;; Commentary: +;; `cj/shell-quote-argument-readable' is the readable-quote helper. +;; When ARGUMENT consists only of characters safe in a shell string, +;; return it unchanged so the surrounding command stays human-readable +;; (compile / test command lines, log inspection). When it contains +;; whitespace or shell metacharacters, fall back to +;; `shell-quote-argument' so the result is safe to interpolate. + +;;; Code: + +(require 'ert) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'system-lib) + +(ert-deftest test-cj-shell-quote-argument-readable-simple-path-unchanged () + "Normal: an alphanumeric path is returned unchanged." + (should (equal (cj/shell-quote-argument-readable "tests/test-foo.el") + "tests/test-foo.el"))) + +(ert-deftest test-cj-shell-quote-argument-readable-test-regex-unchanged () + "Normal: a typical test-name regex with safe punctuation is unchanged." + (should (equal (cj/shell-quote-argument-readable "^test-dev-fkeys-") + "^test-dev-fkeys-"))) + +(ert-deftest test-cj-shell-quote-argument-readable-flags-unchanged () + "Normal: arguments with `=', `+', `:' read as-is (FLAG=value, addr:port)." + (should (equal (cj/shell-quote-argument-readable "FILE=tests/test-foo.el") + "FILE=tests/test-foo.el")) + (should (equal (cj/shell-quote-argument-readable "host:1234") + "host:1234"))) + +(ert-deftest test-cj-shell-quote-argument-readable-spaces-quoted () + "Boundary: an argument containing spaces falls back to shell-quote-argument." + (let ((quoted (cj/shell-quote-argument-readable "path/with space.el"))) + ;; shell-quote-argument either backslash-escapes or wraps in single + ;; quotes; either way the raw input cannot survive verbatim. + (should-not (equal quoted "path/with space.el")) + (should (string-match-p "space" quoted)))) + +(ert-deftest test-cj-shell-quote-argument-readable-shell-metachars-quoted () + "Boundary: arguments with `$', `;', `&', backticks, `*' are quoted." + (dolist (arg '("$HOME" "a;b" "foo&bar" "back`tick`" "glob*")) + (let ((quoted (cj/shell-quote-argument-readable arg))) + (should-not (equal quoted arg))))) + +(ert-deftest test-cj-shell-quote-argument-readable-empty-string-quoted () + "Boundary: empty string is unsafe in a command line and is quoted." + (let ((quoted (cj/shell-quote-argument-readable ""))) + (should-not (equal quoted "")))) + +(provide 'test-system-lib-shell-quote-argument-readable) +;;; test-system-lib-shell-quote-argument-readable.el ends here |
