This post is part of a series about shells in Emacs:


On the previous post, we discussed how to make more explicit functions to call single shell command.

Now, let’s get into more juicy stuff with interactive shells.

Emacs Interactive Shells API

For spawning shell, the main command is: shell (& buffer).

Like for single shell commands, there is some hidden vars we can manipulate to change the behavior.

Here are the important vars we want to manipulate.

default-directorylocation from which to launch shell
explicit-shell-file-name / shell-file-nameshell interpreter exec (e.g. bash …)
explicit-<INTERPRETER>-argsstartup arguments for invoking the shell interactively (e.g. -i)

Note that shell-command-switch is not listed of no use for interactive shells.

Instead, we have the new var explicit-<INTEPRETER>-args that allows passing a list of commands to the shell before it becomes interactive.

You might have spotted it, but in the previous post function eval-with-shell-interpreter already handled this use-case.

Reusing our Wrappers

Hence, we can reuse the wrapper with-shell-interpreter that we defined previously.

There is just a small annoyance: for remote connections, shell prompts systematically for the remote interpreter path.

To disable this behaviour, we have to call it with the default prefix argument (i.e. C-u M-x shell), of programmatically by let-binding current-prefix-arg to '(4).

In this case, explicit-shell-file-name (or shell-file-name as a fallback) is used.

This gives, for example:

(defun my/zsh-local ()
    :path "~"
    :interpreter "zsh"

(defun my/bash-on-raspi ()
   :path "/ssh:pi@raspi:/~"
   :interpreter "bash"
   (let ((current-prefix-arg '(4)))     ; don't prompt for remote interperter path

Another wrapper

That’s still quite cumbersome.

So let’s create another derived helper to prevent repetition.

Click to toggle
;; ------------------------------------------------------------------------

(cl-defun friendly-shell (&key path interpreter interpreter-args command-switch)
  "Create a shell at given PATH, using given INTERPRETER binary."

    (let* ((path (or path default-directory))
           (is-remote (file-remote-p path))
           (interpreter (or interpreter
                            (if is-remote
           (interpreter (prf/tramp/path/normalize interpreter))
           (shell-buffer-basename (friendly-shell--generate-buffer-name is-remote interpreter path))
           (shell-buffer-name (generate-new-buffer-name shell-buffer-name))
           (current-prefix-arg '(4))
           (comint-process-echoes t))
      (shell shell-buffer-name))
    :path path
    :interpreter interpreter
    :interpreter-args interpreter-args))

;; ------------------------------------------------------------------------

(defun friendly-shell--generate-buffer-name (is-remote interpreter path)
  (if is-remote
      (friendly-shell--generate-buffer-name-remote interpreter path)
    (friendly-shell--generate-buffer-name-local interpreter path)))

(defun friendly-shell--generate-buffer-name-local (&optional interpreter _path)
  (if interpreter
      (prf-with-interpreter--get-interpreter-name interpreter)

(defun friendly-shell--generate-buffer-name-remote (intepreter path)
  (let ((vec (tramp-dissect-file-name path)))
    (friendly-shell--generate-buffer-name-remote-from-vec vec)))

(defun friendly-shell--generate-buffer-name-remote-from-vec (vec)
  (let (user host)
     (tramp-file-name-user vec) "@" (tramp-file-name-host vec))))

Please note that we force comint-process-echoes to t to ensure that directory tracking works properly.

Directory tracking (ditrack for short) is the Emacs capability to keep track of current directory when doing a cd.

Also, we embarked functions to help make shell buffer names more explicit.

Our rewritten commands become:

(defun my/zsh-local ()
  (friendly-shell :path "~" :interpreter "zsh"))

(defun my/bash-on-raspi ()
  (friendly-shell :path "/ssh:pi@raspi:/~" :interpreter "bash"))

The code for friendly-shell can be found in package friendly-shell.

Tagged #emacs.