Improve handling of Eshell "for" loops

This fixes some errors with more-complex string forms, and also allows
iterating over sequences other than just lists.

* lisp/eshell/esh-cmd.el (eshell-for-iterate): New function...
(eshell-rewrite-for-command): ... use it.

* test/lisp/eshell/esh-cmd-tests.el (esh-cmd-test/for-loop): Test
multiple values.
(esh-cmd-test/for-loop-string, esh-cmd-test/for-loop-vector): New tests.
(esh-cmd-test/for-loop-mixed-args): Rename.

* test/lisp/eshell/esh-proc-tests.el
(esh-proc-test/sentinel/change-buffer): Make sure all the processes get
cleaned up.
This commit is contained in:
Jim Porter 2024-11-03 11:22:27 -08:00
parent b3c82f939c
commit 08d5994b43
3 changed files with 49 additions and 35 deletions

View file

@ -526,6 +526,20 @@ the second is ignored."
(defvar eshell--local-vars nil
"List of locally bound vars that should take precedence over env-vars.")
(iter-defun eshell-for-iterate (&rest args)
"Iterate over the elements of each sequence in ARGS.
If ARGS is not a sequence, treat it as a list of one element."
(dolist (arg args)
(cond
((stringp arg)
(iter-yield arg))
((listp arg)
(dolist (i arg) (iter-yield i)))
((arrayp arg)
(dotimes (i (length arg)) (iter-yield (aref arg i))))
(t
(iter-yield arg)))))
(defun eshell-rewrite-for-command (terms)
"Rewrite a `for' command into its equivalent Eshell command form.
Because the implementation of `for' relies upon conditional evaluation
@ -533,23 +547,14 @@ of its argument (i.e., use of a Lisp special form), it must be
implemented via rewriting, rather than as a function."
(if (and (equal (car terms) "for")
(equal (nth 2 terms) "in"))
(let ((for-items (make-symbol "for-items"))
(let ((iter-symbol (intern (nth 1 terms)))
(body (car (last terms))))
(setcdr (last terms 2) nil)
`(let ((,for-items
(append
,@(mapcar
(lambda (elem)
(if (listp elem)
(eshell-term-as-value elem)
`(list ,elem)))
(nthcdr 3 terms)))))
(while ,for-items
(let ((,(intern (cadr terms)) (car ,for-items))
(eshell--local-vars (cons ',(intern (cadr terms))
eshell--local-vars)))
,body)
(setq ,for-items (cdr ,for-items)))))))
`(let ((eshell--local-vars (cons ',iter-symbol eshell--local-vars)))
(iter-do (,iter-symbol (eshell-for-iterate
,@(mapcar #'eshell-term-as-value
(nthcdr 3 terms))))
,body)))))
(defun eshell-structure-basic-command (func names keyword test &rest body)
"With TERMS, KEYWORD, and two NAMES, structure a basic command.

View file

@ -319,8 +319,15 @@ processes correctly."
(ert-deftest esh-cmd-test/for-loop ()
"Test invocation of a for loop."
(with-temp-eshell
(eshell-match-command-output "for i in 5 { echo $i }"
"5\n")))
(eshell-match-command-output "for i in 1 2 { echo $i }"
"1\n2\n")))
(ert-deftest esh-cmd-test/for-loop-string ()
"Test invocation of a for loop with complex string arguments."
(let ((eshell-test-value "X"))
(with-temp-eshell
(eshell-match-command-output "for i in a b$eshell-test-value { echo $i }"
"a\nbX\n"))))
(ert-deftest esh-cmd-test/for-loop-list ()
"Test invocation of a for loop iterating over a list."
@ -328,7 +335,13 @@ processes correctly."
(eshell-match-command-output "for i in (list 1 2 (list 3 4)) { echo $i }"
"1\n2\n(3 4)\n")))
(ert-deftest esh-cmd-test/for-loop-multiple-args ()
(ert-deftest esh-cmd-test/for-loop-vector ()
"Test invocation of a for loop iterating over a vector."
(with-temp-eshell
(eshell-match-command-output "for i in `[1 2 3] { echo $i }"
"1\n2\n3\n")))
(ert-deftest esh-cmd-test/for-loop-mixed-args ()
"Test invocation of a for loop iterating over multiple arguments."
(with-temp-eshell
(eshell-match-command-output "for i in 1 2 (list 3 4) { echo $i }"
@ -348,13 +361,6 @@ processes correctly."
"echo $name; for name in 3 { echo $name }; echo $name"
"env-value\n3\nenv-value\n"))))
(ert-deftest esh-cmd-test/for-loop-for-items-shadow ()
"Test that the variable `for-items' isn't shadowed inside for loops."
(with-temp-eshell
(with-no-warnings (setq-local for-items "hello"))
(eshell-match-command-output "for i in 1 { echo $for-items }"
"hello\n")))
(ert-deftest esh-cmd-test/for-loop-lisp-body ()
"Test invocation of a for loop with a Lisp body form."
(with-temp-eshell

View file

@ -135,16 +135,19 @@
(ert-deftest esh-proc-test/sentinel/change-buffer ()
"Check that changing the current buffer while running a command works.
See bug#71778."
(eshell-with-temp-buffer bufname ""
(with-temp-eshell
(let (eshell-test-value)
(eshell-insert-command
(concat (format "for i in 1 2 {sleep 1; echo hello} > #<%s>; " bufname)
"setq eshell-test-value t"))
(with-current-buffer bufname
(eshell-wait-for (lambda () eshell-test-value))
(should (equal (buffer-string) "hellohello")))
(eshell-match-command-output "echo goodbye" "\\`goodbye\n")))))
(let ((starting-process-list (process-list)))
(eshell-with-temp-buffer bufname ""
(with-temp-eshell
(let (eshell-test-value)
(eshell-insert-command
(concat (format "for i in 1 2 {sleep 1; echo hello} > #<%s>; "
bufname)
"setq eshell-test-value t"))
(with-current-buffer bufname
(eshell-wait-for (lambda () eshell-test-value))
(should (equal (buffer-string) "hellohello")))
(should (equal (process-list) starting-process-list))
(eshell-match-command-output "echo goodbye" "\\`goodbye\n"))))))
;; Pipelines