4

Is this possible with stock emacs:

I'm editing a .txt file that has a snippet of bash code When I do C-x b RET I can switch to a preopened M-x shell buffer named guesuser@productionServerShellSession I have the snippet of bash code selected in a .txt file. It's a multi line pipe command ending with \ on each line. I often find myself copying the region, switching buffer and pasting and pressing enter to run the snippet in shell.

Can this be made easier with fewer key strokes?

What I did: Researched https://stackoverflow.com/questions/6286579/emacs-shell-mode-how-to-send-region-to-shell

4 Answers 4

5

Emacs provides the function process-send-region to do this. If your buffer always has the same name, you can hard-code it in:

(defun my-send (beg end)
  (interactive "r")
  (process-send-region "guesuser@productionServerShellSession" beg end))

The first line defines a new function, my-send, with two arguments, beg and end. The second line declares that this function is interactive, and will use the current region (that's what the "r" flag does). So when we call it, beg and end are filled with the start and end points of the active region.

The last line sends that region to your shell.

If you want to send the region to different buffers, you'll need to add some sort of logic to accommodate this. As a simple example, you could store your target buffer in a variable:

(defun my-set-target ()
   (interactive)
   (setq my-target (buffer-name)))

Calling this command will store the current buffer name in the variable my-target. Modifying the original command:

(defun my-send (beg end)
  (interactive "r")
  (process-send-region my-target beg end))

There are lots of ways you could smooth these out, to handle various errors (what if my-target hasn't been defined yet, or what if the region doesn't include a terminating newline, etc). But that should get you started.

Actually, I've been wanting this for a while, so I made a more complete version:

(defun tws-region-to-process (arg beg end)
  "Send the current region to a process buffer.
The first time it's called, will prompt for the buffer to
send to. Subsequent calls send to the same buffer, unless a
prefix argument is used (C-u), or the buffer no longer has an
active process."
  (interactive "P\nr")
  (if (or arg ;; user asks for selection
          (not (boundp 'tws-process-target)) ;; target not set
          ;; or target is not set to an active process:
          (not (process-live-p (get-buffer-process
                                tws-process-target)))) 
      (setq tws-process-target
            (completing-read
             "Process: "
             (seq-map (lambda (el) (buffer-name (process-buffer el)))
                      (process-list)))))
  (process-send-region tws-process-target beg end))
9
  • amazing how elisp can do so much in so few lines of code Jan 4, 2018 at 17:22
  • I wish the it would let me specify the buffer name in the "completing-read", I have four shell buffers with ssh tunnels running in them, they are named using M-x shell , rename-buffer ... root@remoteserverShell , guest@remoteserverShell. These buffers are presented to me as shell. shell <1> shell <2>. and it would be better if the names were distinguishable Jan 4, 2018 at 17:27
  • btw this is a really cool answer overall Jan 4, 2018 at 17:30
  • agreed - try the edited version!
    – Tyler
    Jan 4, 2018 at 18:19
  • wondering about alternatives to "process-send-region" because the sent text is not echoed in the shell, so I would blindly have to press enter to realize some text was sent to the prompt Jan 4, 2018 at 20:03
2
  • You can write a command that does all of that. Someone else here will no doubt give you code for that.

  • Or you can use a keyboard macro: just use <f3> and <f4> to record what you are already doing and <f4> to replay it.

    See the Emacs manual, node Keyboard Macros.

  • You can also simplify the part about recopying the region by using the secondary selection. It stays the same regardless of where the cursor is, until you change it on purpose. So you can paste/yank it any number of times, regardless of where you are.

    Press and hold M- (Alt key) while you use the mouse to select the text as the secondary selection.

    If you use library second-sel.el then you can use command yank-secondary (or secondary-dwim, for more flexibility) to yank (paste) the secondary selection. I bind it to C-M-y.

    See the Emacs manual, node Secondary Selection.

  • Another way to simplify the code-copying part is to put that code in a file and insert the file each time you need the code. C-x i (command insert-file) does that.

    See the Emacs manual, node Misc File Ops.

2

This is a modification to Tyler's solution. It addresses two issues with the original:

  1. When a process has no buffer, which I encountered for emacs-server and ispell processes, function process-buffer returns nil, which causes function buffer-name to return the name of the current buffer, which will never be what you want, and which will clutter your completing-read selection with as many copies of the current buffer as you have processes that lack buffers.

  2. When no process buffers exist, a message to that effect should appear.

Both Tyler's original solution and this modification work not only for shell buffers, but also for term and ansi-term buffers, with a small difference. For shell buffers, the region sent will not appear in the target buffer, but for term and ansi-term buffers, that text will appear.

(defun tws-region-to-process (arg beg end) "Send the current region to a process buffer. The first time it's called, will prompt for the buffer to send to. Subsequent calls send to the same buffer, unless a prefix argument is used (C-u), or the buffer no longer has an active process." (interactive "P\nr") (when (or arg ;; user asks for selection (not (boundp 'tws-process-target)) ;; target not set ;; or target is not set to an active process: (not (process-live-p (get-buffer-process tws-process-target)))) (let (procs buf) (setq procs (remove nil (seq-map (lambda (el) (when (setq buf (process-buffer el)) (buffer-name buf))) (process-list)))) (if (not procs) (error "No process buffers currently open.") (setq tws-process-target (completing-read "Process: " procs))))) (process-send-region tws-process-target beg end))

What remains missing from this answer, and from Tyler's original, is how to send a region to an e-shell buffer. As Tyler mentioned in the comments, eshell uses a different approach that would require a different solution.

4
  • interesting answer. can you see the command in the target shell buffer after it's sent? (is it echoed out). that was what bugged me in Tylers solution Jan 10, 2018 at 13:32
  • The original question referred to shell-mode, not eshell, so I'm not sure that dealing with eshell is relevant. In any case, eshell is implemented completely differently, and doesn't use an external process, so if you want to send text to it you'll have to use another route. process- or comint- functions won't do anything.
    – Tyler
    Jan 10, 2018 at 19:06
  • @joshsverns: The posted solutions work not only for shell, but also for term and ansi-term. When using the solutions to send a region to a term or ansi-term process, the region text does appear. I don't know why that isn't the case when sending to a shell process. Jan 11, 2018 at 16:24
  • @Tyler: Thanks, for creating the solution function, and for the correction. Jan 11, 2018 at 16:28
1

The philosopher's question, does Emacs extended through stock functions still constitute "stock emacs"? Here are a handful of functions for these kinds of tasks which rely only on stock Emacs (no dash, s, or whathaveyou).

The idea works by defining an "on-demand window" and a setter function:

(defvar my-on-demand-window nil
  "Target on-demand window.

An on-demand window is one which you wish to return to within the
current Emacs session but whose importance doesn't warrant a
permanent binding.")

(defun my-on-demand-window-set ()
  "Set the value of the on-demand window to current window."
  (interactive)
  (setq my-on-demand-window (selected-window))
  (message "Set on-demand window to: %s" my-on-demand-window))

With the window stored, you can do useful things like jump to it from anywhere:

(defun my-on-demand-window-goto ()
  "Make `my-on-demand-window' current."
  (interactive)
  (let ((win my-on-demand-window))
    (unless win (error "No on-demand window set! See `my-on-demand-window-set'."))
    (if (eq (selected-window) my-on-demand-window)
        (error "Already in `my-on-demand-window'"))
    (let ((frame (window-frame win)))
      (raise-frame frame)
      (select-frame frame)
      (select-window win))))

But more to your question, you can send a line or region to the on-demand window. It's set up here to insert the region, or the current line if no region is selected, followed by a newline-and-indent. You can modify the behavior after insert to your taste; remove newline-and-indent if you simply want to insert; use comint-send-input instead to press "RET" in a comint buffer (i.e. shell); use eshell-send-input to press "RET" in an eshell buffer; etc.

(defun my-send-line-or-region (&optional beg end buff)
  "Send region defined by BEG and END to BUFF.

Use current region if BEG and END not provided.  If no region
provided, send entire line.  Default BUFF is that displayed in
`my-on-demand-window'."
  (interactive (if (use-region-p)
                   (list (region-beginning) (region-end) nil)
                 (list nil nil nil)))
  (let* ((beg (or beg (if (use-region-p) (region-beginning)) nil))
         (end (or end (if (use-region-p) (region-end)) nil))
         (substr (string-trim
                  (or (and beg end (buffer-substring-no-properties beg end))
                     (buffer-substring-no-properties (line-beginning-position) (line-end-position)))))
         (buff (or buff (window-buffer my-on-demand-window))))
    (if substr
        (with-selected-window my-on-demand-window
          (setq-local window-point-insertion-type t)
          (insert substr)
          (end-of-line)
          (newline-and-indent))  ; <-- change this line here for behavior "after insert"
      (error "Invalid selection"))))

I bind it to a key like F5:

(global-set-key (kbd "<f5>") '(lambda () (interactive) (progn (funcall 'my-send-line-or-region))))

Although not stock, I would be remiss if I didn't mention right-click-context. The following binds the send region command to a mouse right-click context menu (less keystrokes, right? :). You can highlight a region with the mouse and immediately send it to the on-demand window:

(use-package right-click-context
  :config
  (right-click-context-mode 1)

  (add-to-list 'right-click-context-global-menu-tree
               '("Send region to on-demand-window"
                 :call (my-send-line-or-region))))

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.