4

I'm a Vim user who is trying to migrate to emacs. On Vim, I've been using the following vimscript function to execute code:

function! ExecuteOnTerminal(type) range
    if (&ft=='bash' || &ft=='sh')
        :vert botright %terminal bash
    elseif (&ft=='python')
        :vert botright %terminal python3
    elseif (&ft=='javascript')
        :vert botright %terminal node
    endif
endfunction

This function works fine, it checks the current file type and then executes the whole file on a split window.

I'd like to replicate this function on emacs, so I've created the following function that only works partially:

(global-set-key (kbd "C-c e") 'execute-code)
(defun execute-code ()
  "Function that executes code"
  (interactive)
  (write-region (point-min) (point-max) "/tmp/file")
  (case major-mode
    ('sh-mode (setq code (concat "bash /tmp/file")))
    ('python-mode (setq code (concat "python3 /tmp/file")))
    ('js2-mode (setq code (concat "node /tmp/file")))
    (t (print major-mode)))

    (print code)
    (with-current-buffer (pop-to-buffer "*output*")
      (erase-buffer)
      (insert (shell-command-to-string code))
      )
  )

Even though I can execute bash, python and node.js code with this function. It waits for the whole code to execute before showing the results on a split window. For example, let's say that I open the following bash file and I press C-c e:

echo "test"
sleep 2
echo "test"
sleep 2
echo "test"
sleep 2
echo "test"
sleep 2
echo "test"
sleep 2
echo "test"

Even though emacs will execute this file, the command shell-command-to-string is not a good substitute for the terminal vim command. The terminal command prints the results while the file is being executed, while the shell-command-to-string waits 10 seconds until the execution finishes to display the results at once. Is there any workaround for this issue with emacs? Is there any function I can use with this purpose instead of the shell-command-to-string function?

2 Answers 2

4

Naturally shell-command-to-string cannot return a string until the shell command in question has exited.

In Emacs you generally can and should work directly with buffers -- there is often no need to introduce strings as intermediate data.

In your case, both shell-command and async-shell-command accept an optional OUTPUT-BUFFER argument, and the asynchronous version is the one you're after if you want the process to run in the background and produce output as it happens.

(async-shell-command COMMAND &optional OUTPUT-BUFFER ERROR-BUFFER)

Alternatively, quoting its docstring:

In Elisp, you will often be better served by calling start-process directly, since it offers more control and does not impose the use of a shell (with its need to quote arguments).

For all of the details of working with processes in elisp, start at C-hig (elisp)Processes

6
  • Thanks! The async-shell-command does what I need regarding the code execution. However, it splits the window horizontally in a non-proportional way... For me, it was important to split the window vertically in half as it was in my original question. Do you know if there's any way of using the async-shell-command in a vertical split window?
    – raylight
    Dec 5, 2022 at 3:45
  • If you display the buffer in the same manner that you were doing already (which I presume was ok?), or any other approach to displaying the buffer yourself after creating it, Emacs wouldn't have any reason to split the windows further.
    – phils
    Dec 5, 2022 at 5:04
  • The way I was splitting the window before doesn't work anymore... It splits an empty window on the vertical and show the output results in a second horizontal window... That's why I thought I'd need a different approach after changing to the async-shell-command
    – raylight
    Dec 5, 2022 at 5:10
  • You end up with two windows showing the same buffer? (If that's not what you mean then you're presumably not actually passing the new buffer you've created as the output-buffer parameter.)
    – phils
    Dec 5, 2022 at 5:37
  • No, I end up with the horizontal split showing the code output while it's running and the vertical split doesn't display anything. That when I try to use the async-shell-command the way I was doing before. If I just use it directly it'll create the horizontal split only (and execute the code).
    – raylight
    Dec 5, 2022 at 5:40
4

Normally for that style of work, there's the concept of inferior shells, where Emacs launches an inferior process (a.k.a. a subprocess) for an interpreter linked to a buffer that shows a REPL and gives you keybindings to pass on pieces of code (statement at point, whole buffer, selected region, etc.) from a file buffer to the interpreter. There's support for various languages.

Take Python, for example:

Emacs session using Inferior Python

In the above, I show how to launch the inferior process and REPL using run-python from a buffer in python-mode. The available keybindings for python-mode major mode can be seen by doing C-h m in that buffer. You can see C-M-x can be used to send the statement at point to the inferior process, C-c C-c can be used to send the whole buffer, and C-c C-r can be used to send the selected region.

Surprisingly, sh-mode doesn't follow this convention. It's not hard to add the basics, though:

(defun bash-comint-send-thing (what)
  (when (get-buffer "*bash*")
      (let* ((thing (thing-at-point what))
             (quoted-thing (string-replace "'" "'\\''" thing))
             (quoted-thing (string-replace "\n" "'$'\\n''" quoted-thing))
             (quoted-thing (concat "'" quoted-thing "'"))
             (wrapped-command (concat "echo; eval " quoted-thing "\n")))
        (comint-send-string "*bash*" wrapped-command))))

(defun bash-comint-send-line ()
  (interactive)
  (bash-comint-send-thing 'line))

(defun bash-comint-send-buffer ()
  (interactive)
  (bash-comint-send-thing 'buffer))

(define-key sh-mode-map (kbd "C-c e") #'bash-comint-send-line)
(define-key sh-mode-map (kbd "C-c b") #'bash-comint-send-buffer)

Here's how it can be used, tested with the code in the question:

Emacs session using an inferior shell for the shell

Essentially, do M-x comint-run which will prompt for an interactive command (intentionally a shell/REPL) and open a buffer bound to that subprocess. Then use the elisp code above or similar to send it code to evaluate.

Output is also shown to appear immediately, instead of having to wait for the evaluation to finish.

Your Answer

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

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