Subject: eshell -- programmatically send input -- feature request

From: Keith David Bershatsky <[email-masked]>
Date: Sun, 25 Dec 2016 11:00:16 -0800

As far as I am aware, users of eshell have been limited to sending input programmatically by inserting the command into the eshell buffer (at the command prompt) and then executing `eshell-send-input`.  Some users (like myself) may feel that such a solution is lo-tech -- i.e., not very eloquent.  Perhaps this is because I, and perhaps others, am/are spoiled by functions available when running shell such as:

    (let ((buf (shell)))
      (comint-send-string buf "ls -la")
      (comint-send-input))

or

    (let ((buf (shell)))
      (comint-simple-send buf "ls -la"))

Here is a link to a thread entitled "How to programmatically execute a command in eshell?":

http://emacs.stackexchange.com/questions/7617/how-to-programmatically-execute-a-command-in-eshell

And the accepted answer is as follows:

    (with-current-buffer "*eshell*"
      (eshell-return-to-prompt)
      (insert "ls")
      (eshell-send-input))

The following is an example of how this new feature might be implemented:

SAMPLE USAGE:  (eshell-send-input nil nil nil "ls -la /")

(require 'eshell)

(defun eshell-send-input (&optional use-region queue-p no-newline input-string-a)
  "Send the input received to Eshell for parsing and processing.
After `eshell-last-output-end', sends all text from that marker to
point as input.  Before that marker, calls `eshell-get-old-input' to
retrieve old input, copies it to the end of the buffer, and sends it.
-  If USE-REGION is non-nil, the current region (between point and mark)
will be used as input.
-  If QUEUE-P is non-nil, input will be queued until the next prompt,
rather than sent to the currently active process.  If no process, the
input is processed immediately.
-  If NO-NEWLINE is non-nil, the input is sent without an implied final
newline."
  (interactive "P")
  ;; Note that the input string does not include its terminal newline.
  (let ((proc-running-p
          (and (eshell-interactive-process)
               (not queue-p)))
        (inhibit-point-motion-hooks t)
        after-change-functions)
    (unless (and proc-running-p
                 (not (eq (process-status (eshell-interactive-process)) 'run)))
      (if (or proc-running-p
              (>= (point) eshell-last-output-end))
        (goto-char (point-max))
        ;; This is for a situation when point is before `point-max'.
        (let ((copy (or input-string-a (eshell-get-old-input use-region))))
          (goto-char eshell-last-output-end)
          (insert-and-inherit copy)))
      (unless (or no-newline
                  (and eshell-send-direct-to-subprocesses
                       proc-running-p))
        (insert-before-markers-and-inherit ?\n))
      (if proc-running-p
        (progn
          (eshell-update-markers eshell-last-output-end)
          (if (or eshell-send-direct-to-subprocesses
                  (= eshell-last-input-start eshell-last-input-end))
            (unless no-newline
              (process-send-string (eshell-interactive-process) "\n"))
                (process-send-region (eshell-interactive-process)
                   eshell-last-input-start
                   eshell-last-input-end)))
        (if (and (null input-string-a) (= eshell-last-output-end (point)))
          ;; This next line is for a situation when nothing is there -- just make a new command prompt.
          (run-hooks 'eshell-post-command-hook)
          (let (input)
            (eshell-condition-case err
              (progn
                (setq input (or input-string-a
                                (buffer-substring-no-properties
                                   eshell-last-output-end (1- (point)))))
                (run-hook-with-args 'eshell-expand-input-functions
                        eshell-last-output-end (1- (point)))
                (let ((cmd (eshell-parse-command-input
                             eshell-last-output-end (1- (point)) nil input-string-a)))
                  (when cmd
                    (eshell-update-markers eshell-last-output-end)
                    (setq input (buffer-substring-no-properties
                                  eshell-last-input-start
                                  (1- eshell-last-input-end)))
                    (run-hooks 'eshell-input-filter-functions)
                    (and (catch 'eshell-terminal
                           (ignore
                             (if (eshell-invoke-directly cmd)
                               (eval cmd)
                               (eshell-eval-command cmd input))))
                         (eshell-life-is-too-much)))))
                  (quit
                    (eshell-reset t)
                    (run-hooks 'eshell-post-command-hook)
                    (signal 'quit nil))
                  (error
                    (eshell-reset t)
                    (eshell-interactive-print
                      (concat (error-message-string err) "\n"))
                    (run-hooks 'eshell-post-command-hook)
                    (insert-and-inherit input)))))))))

(defun eshell-parse-command-input (beg end &optional args input-string-b)
  "Parse the command input from BEG to END.
The difference is that `eshell-parse-command' expects a complete
command string (and will error if it doesn't get one), whereas this
function will inform the caller whether more input is required.
-  If nil is returned, more input is necessary (probably because a
multi-line input string wasn't terminated properly).  Otherwise, it
will return the parsed command."
  (let (delim command)
    (if (setq delim (catch 'eshell-incomplete
                      (ignore
                        (setq command (eshell-parse-command (cons beg end) args t input-string-b)))))
      (ignore
        (message "Expecting completion of delimiter %c ..."
          (if (listp delim)
              (car delim)
            delim)))
      command)))

(defun eshell-parse-command (command &optional args toplevel input-string-c)
  "Parse the COMMAND, adding ARGS if given.
COMMAND can either be a string, or a cons cell demarcating a buffer
region.  TOPLEVEL, if non-nil, means that the outermost command (the
user's input command) is being parsed, and that pre and post command
hooks should be run before and after the command."
  (let* (
      eshell--sep-terms
      (terms
        (if input-string-c
          (eshell-parse-arguments--temp-buffer input-string-c)
          (append
            (if (consp command)
              (eshell-parse-arguments (car command) (cdr command))
              (let ((here (point))
                    (inhibit-point-motion-hooks t))
                (with-silent-modifications
                  ;; FIXME: Why not use a temporary buffer and avoid this
                  ;; "insert&delete" business?  --Stef
                  (insert command)
                  (prog1
                      (eshell-parse-arguments here (point))
                    (delete-region here (point))))))
            args)))
      (commands
        (mapcar
          (function
            (lambda (cmd)
              (setq cmd (if (or (not (car eshell--sep-terms))
                                (string= (car eshell--sep-terms) ";"))
                          (eshell-parse-pipeline cmd)
                          `(eshell-do-subjob
                              (list ,(eshell-parse-pipeline cmd)))))
              (setq eshell--sep-terms (cdr eshell--sep-terms))
              (if eshell-in-pipeline-p
                cmd
                `(eshell-trap-errors ,cmd))))
          (eshell-separate-commands terms "[&;]" nil 'eshell--sep-terms))) )
    (let ((cmd commands))
      (while cmd
        (if (cdr cmd)
            (setcar cmd `(eshell-commands ,(car cmd))))
        (setq cmd (cdr cmd))))
    (if toplevel
      `(eshell-commands (progn
                                (run-hooks 'eshell-pre-command-hook)
                                (catch 'top-level (progn ,@commands))
                                (run-hooks 'eshell-post-command-hook)))
      (macroexp-progn commands))))

(defun eshell-parse-arguments--temp-buffer (input-string-d)
  "Parse all of the arguments at point from BEG to END.
Returns the list of arguments in their raw form.
Point is left at the end of the arguments."
  (with-temp-buffer
    (insert input-string-d)
    (let ((inhibit-point-motion-hooks t)
          (args (list t))
          delim)
      (with-silent-modifications
        (remove-text-properties (point-min) (point-max)
                                '(arg-begin nil arg-end nil))
        (goto-char (point-min))
        (if (setq
             delim
             (catch 'eshell-incomplete
               (while (not (eobp))
                 (let* ((here (point))
                        (arg (eshell-parse-argument)))
                   (if (= (point) here)
                       (error "Failed to parse argument '%s'"
                              (buffer-substring here (point-max))))
                   (and arg (nconc args (list arg)))))))
            (throw 'eshell-incomplete (if (listp delim)
                                          delim
                                        (list delim (point) (cdr args)))))
        (cdr args)))))