This article is part of a multi-post 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: by default command shell will prompt for user to specify interpreter path.

This is cumbersome to have to precise it every time.

We might prefer to just have it default to shell-file-name for local shells and default-remote-shell-interpreter for remote ones.

If we want to change location, it would be more practical to create a command that explicitly defines the :interpreter.

To disable shell prompting for interpreter path, we have to call it with the default prefix argument (i.e. C-u M-x shell).

To reproduce this behavior programmatically, we’d have to let bind current-prefix-arg to '(4).

This gives, for example:

(defun my/zsh-local ()
   :path "~"
   :interpreter "zsh"
   (let ((current-prefix-arg '(4)))

(defun my/bash-on-raspi ()
   :path "/ssh:pi@raspi:/~"
   :interpreter "bash"
   (let ((current-prefix-arg '(4)))

Another wrapper

That’s still quite cumbersome.

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

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

(cl-defun prf-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 (prf-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 prf-shell--generate-buffer-name (is-remote interpreter path)
  (if is-remote
      (prf-shell--generate-buffer-name-remote interpreter path)
    (prf-shell--generate-buffer-name-local interpreter path)))

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

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

(defun prf-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 ()
  (prf-shell :path "~" :interpreter "zsh"))

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

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

Tagged #emacs.