master 06241611033 5/8: Add 'apply-lines' and 'map-lines' Eshell commands

Jim Porter <[email protected]>
Newsgroups gmane.emacs.diffs
Message-ID <[email protected]>
branch: master
commit 062416110333fbc34f7af81402021357676b0e43
Author: Jim Porter <[email protected]>
Commit: Jim Porter <[email protected]>

    Add 'apply-lines' and 'map-lines' Eshell commands
    
    * lisp/eshell/esh-worker.el (eshell-map-lines-worker)
    (eshell-apply-lines-worker): New structs.
    (eshell-map-lines-worker--apply): New generic function.
    (eshell-output-object-to-target, eshell-close-target): New methods.
    (eshell/map-lines, eshell/apply-lines): New functions.
    
    * test/lisp/eshell/esh-worker-tests.el (eshell-test-line-number): New
    defvar...
    (eshell-test-number): ... use it in this new function.
    (esh-worker-test/map-lines/eshell-one-line)
    (esh-worker-test/map-lines/eshell-multiple-lines)
    (esh-worker-test/map-lines/eshell-multiple-batches)
    (esh-worker-test/map-lines/external-one-line)
    (esh-worker-test/map-lines/external-multiple-lines)
    (esh-worker-test/map-lines/numbers)
    (esh-worker-test/map-lines/numeric-conversion)
    (esh-worker-test/apply-lines/eshell-one-line)
    (esh-worker-test/apply-lines/eshell-multiple-lines)
    (esh-worker-test/apply-lines/external-one-line)
    (esh-worker-test/apply-lines/external-multiple-lines)
    (esh-worker-test/apply-lines/numbers)
    (esh-worker-test/apply-lines/numeric-conversion): New tests.
---
 lisp/eshell/esh-worker.el            | 105 ++++++++++++++++++++++++++++++++
 test/lisp/eshell/esh-worker-tests.el | 115 +++++++++++++++++++++++++++++++++++
 2 files changed, 220 insertions(+)

diff --git a/lisp/eshell/esh-worker.el b/lisp/eshell/esh-worker.el
index 16233368af0..69319b3f7e7 100644
--- a/lisp/eshell/esh-worker.el
+++ b/lisp/eshell/esh-worker.el
@@ -33,6 +33,25 @@
 ;; also express this explicitly, as in:
 ;;
 ;;   echo hi | accumulate #'upcase
+;;
+;;;_* `map-lines' worker
+;;
+;; You can also map each line of output to a function, similar to how
+;; `mapcar' works.  For example, you could "quote" some output for
+;; pasting into an email like this:
+;;
+;;   cat some-file.txt | map-lines (lambda (i) (format "> %s" i))
+;;
+;; This also shows how you use lambda functions as pipe targets in
+;; Eshell.
+;;
+;;;_* `apply-lines' worker
+;;
+;; Finally, you can apply each line of output as successive arguments to
+;; a function.  For example, to sum up a list of numbers written one per
+;; line:
+;;
+;;   cat numbers.txt | apply-lines #'+
 
 ;;; Code:
 
@@ -190,5 +209,91 @@ this structure only receives a single non-string value as input, it will
 pass the value unaltered to FUNCTION."
   (eshell-accumulate-worker-create :function function))
 
+;; Map-lines worker
+
+(cl-defstruct (eshell-map-lines-worker
+               (:include eshell-worker)
+               (:constructor eshell-map-lines-worker-create))
+  "A worker that calls a Lisp function once for each line.
+When outputting string data to this worker, it will call a Lisp
+FUNCTION once per line of text.  When outputting other data types to
+this worker (e.g. lists), it will call FUNCTION once with the
+specified value."
+  (function nil :read-only t)
+  (current-line nil))
+
+(cl-defgeneric eshell-map-lines-worker--apply (line target)
+  (let ((function (eshell-map-lines-worker-function target)))
+    (eshell--apply-print function (list line))))
+
+(cl-defmethod eshell-output-object-to-target
+  (object (target eshell-map-lines-worker))
+  "Send OBJECT to the map-lines worker TARGET.
+This calls the function associated with the worker.
+
+The returned value is the OBJECT in the form that it was actually
+sent to TARGET (e.g. a string representing OBJECT)."
+  (if (stringp object)
+      (let (;; Prepend any saved text from the current line to the new text.
+            (pending (concat (eshell-map-lines-worker-current-line target)
+                             object))
+            line-begin line-end line)
+        ;; Pass each full line of text to FUNCTION.
+        (while (setq line-end (string-search "\n" pending line-begin))
+          (setq line (eshell-mark-numeric-string
+                      (substring pending line-begin line-end))
+                line-begin (1+ line-end))
+          (eshell-map-lines-worker--apply line target))
+        ;; Save any remaining text after the last newline for next time.
+        (setf (eshell-map-lines-worker-current-line target)
+              (and (length> pending (or line-begin 0))
+                   (substring pending line-begin))))
+    (eshell-map-lines-worker--apply object target))
+  object)
+
+(cl-defmethod eshell-close-target ((target eshell-map-lines-worker) _status)
+  (when-let* ((last-line (eshell-map-lines-worker-current-line target)))
+    (eshell-map-lines-worker--apply (eshell-mark-numeric-string last-line)
+                                    target)))
+
+(defun eshell/map-lines (function)
+  "Map each line of output of another command to FUNCTION.
+When outputting string data to this worker, it will call FUNCTION once
+per line of text.  When outputting other data types to this
+worker (e.g. lists), it will call FUNCTION once with the specified
+value."
+  (eshell-map-lines-worker-create :function function))
+
+;; Apply-lines worker
+
+(cl-defstruct
+    (eshell-apply-lines-worker
+     (:include eshell-map-lines-worker)
+     (:constructor eshell-apply-lines-worker-create))
+  "A worker that calls a Lisp function with each line as an argument.
+This worker calls a Lisp FUNCTION once, with each line of string data
+corresponding to one argument passed to the fuction.  When outputting
+other data types to this worker (e.g. lists), each object is passed as a
+single argument to FUNCTION."
+  args)                              ; Arguments stored in reverse order
+
+(cl-defmethod eshell-map-lines-worker--apply
+    (line (target eshell-apply-lines-worker))
+  (push line (eshell-apply-lines-worker-args target)))
+
+(cl-defmethod eshell-close-target ((target eshell-apply-lines-worker) _status)
+  (cl-call-next-method)
+  (let ((function (eshell-map-lines-worker-function target)))
+    (eshell--apply-print
+     function (nreverse (eshell-apply-lines-worker-args target)))))
+
+(defun eshell/apply-lines (function)
+  "Call a Lisp FUNCTION with each line of output as an argument.
+This worker calls a Lisp FUNCTION once, with each line of string data
+corresponding to one argument passed to the fuction.  When outputting
+other data types to this worker (e.g. lists), each object is passed as a
+single argument to FUNCTION."
+  (eshell-apply-lines-worker-create :function function))
+
 (provide 'esh-worker)
 ;;; esh-worker.el ends here
diff --git a/test/lisp/eshell/esh-worker-tests.el b/test/lisp/eshell/esh-worker-tests.el
index 831a3269480..7dfca9859cd 100644
--- a/test/lisp/eshell/esh-worker-tests.el
+++ b/test/lisp/eshell/esh-worker-tests.el
@@ -26,8 +26,17 @@
 (require 'eshell-tests-helpers
          (ert-resource-file "eshell-tests-helpers"))
 
+(defvar eshell-test-line-number nil)
+
+(defun eshell-test-number (line)
+  "Return LINE with a line number prepended."
+  (format "%2d %s" (incf eshell-test-line-number) line))
+
 ;;; Tests:
 
+
+;; Basic worker pipelines
+
 (ert-deftest esh-worker-test/pipe/eshell-to-function ()
   "Test that piping an Eshell command to a function works."
   (with-temp-eshell
@@ -81,4 +90,110 @@
      "echo hi | #'upcase | (lambda (i) (concat \"> \" i))"
      "\\`> HI\n\\'")))
 
+
+;; `map-lines' pipelines
+
+(ert-deftest esh-worker-test/map-lines/eshell-one-line ()
+  "Test that piping a single line to `map-lines' works."
+  (let ((eshell-test-line-number 0))
+    (with-temp-eshell
+      (eshell-match-command-output
+       "echo hi | map-lines #'eshell-test-number"
+       "\\` 1 hi\n\\'"))))
+
+(ert-deftest esh-worker-test/map-lines/eshell-multiple-lines ()
+  "Test that piping multiple lines to `map-lines' works.
+It should call the mapped function once per line."
+  (let ((eshell-test-line-number 0))
+    (with-temp-eshell
+      (eshell-match-command-output
+       "echo 'hi\nbye' | map-lines #'eshell-test-number"
+       "\\` 1 hi\n 2 bye\n\\'"))))
+
+(ert-deftest esh-worker-test/map-lines/eshell-multiple-batches ()
+  "Test that piping multiple batches of lines to `map-lines' works.
+It should call the mapped function once per line, reassembling lines as
+needed."
+  (let ((eshell-test-line-number 0))
+    (with-temp-eshell
+      (eshell-match-command-output
+       "{echo 'hi\nhel'; echo 'lo\nhey'} | map-lines #'eshell-test-number"
+       "\\` 1 hi\n 2 hello\n 3 hey\n\\'"))))
+
+(ert-deftest esh-worker-test/map-lines/external-one-line ()
+  "Test that piping a single external line to `map-lines' works."
+  (let ((eshell-test-line-number 0))
+    (with-temp-eshell
+      (eshell-match-command-output
+       "*echo hi | map-lines #'eshell-test-number"
+       "\\` 1 hi\n\\'"))))
+
+(ert-deftest esh-worker-test/map-lines/external-multiple-lines ()
+  "Test that piping multiple external lines to `map-lines' works.
+It should call the mapped function once per line."
+  (let ((eshell-test-line-number 0))
+    (with-temp-eshell
+      (eshell-match-command-output
+       "*echo 'hi\nbye' | map-lines #'eshell-test-number"
+       "\\` 1 hi\n 2 bye\n\\'"))))
+
+(ert-deftest esh-worker-test/map-lines/numbers ()
+  "Test that piping numbers to `map-lines' passes them to the mapped function."
+  (with-temp-eshell
+    (eshell-match-command-output
+     "{echo 10; echo 20} | map-lines #'1+"
+     "\\`11\n21\n\\'")))
+
+(ert-deftest esh-worker-test/map-lines/numeric-conversion ()
+  "Test that `map-lines' converts numeric strings when possible."
+  (with-temp-eshell
+    (eshell-match-command-output
+     "{echo '10\n1'; echo '5\n20'} | map-lines #'1+"
+     "\\`11\n16\n21\n\\'")))
+
+
+;; `apply-lines' pipelines
+
+(ert-deftest esh-worker-test/apply-lines/eshell-one-line ()
+  "Test that piping a single line to `apply-lines' works."
+  (with-temp-eshell
+    (eshell-match-command-output "echo hi | apply-lines #'upcase"
+                                 "\\`HI\n\\'")))
+
+(ert-deftest esh-worker-test/apply-lines/eshell-multiple-lines ()
+  "Test that piping multiple lines to `apply-lines' works.
+It should pass each line as an argument to the applied function."
+  (with-temp-eshell
+    (eshell-match-command-output
+     "echo 'o\ni\nfoobar' | apply-lines #'string-replace"
+     "\\`fiibar\n\\'")))
+
+(ert-deftest esh-worker-test/apply-lines/external-one-line ()
+  "Test that piping a single external line to `apply-lines' works."
+  (with-temp-eshell
+    (eshell-match-command-output "*echo hi | apply-lines #'upcase"
+                                 "\\`HI\n\\'")))
+
+(ert-deftest esh-worker-test/apply-lines/external-multiple-lines ()
+  "Test that piping multiple external lines to `apply-lines' works.
+It should pass each line as an argument to the applied function."
+  (with-temp-eshell
+    (eshell-match-command-output
+     "*echo 'o\ni\nfoobar' | apply-lines #'string-replace"
+     "\\`fiibar\n\\'")))
+
+(ert-deftest esh-worker-test/apply-lines/numbers ()
+  "Test that piping numbers to `apply-lines' passes them to the function."
+  (with-temp-eshell
+    (eshell-match-command-output
+     "{echo 5; echo 8; echo 13} | apply-lines #'+"
+     "\\`26\n\\'")))
+
+(ert-deftest esh-worker-test/apply-lines/numeric-conversion ()
+  "Test that `apply-lines' converts numeric strings when possible."
+  (with-temp-eshell
+    (eshell-match-command-output
+     "{echo '10\n1'; echo '5\n20'} | apply-lines #'+"
+     "\\`45\n\\'")))
+
 ;;; esh-io-tests.el ends here
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.