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

Emacs as a terminal emulator

Emacs can act as a powerful terminal emulator.

It can spawning interactive shells (with shell-mode and term-mode) and execute single shell commands.

This post will focus on single shell commands.

For interactive shells, see next post.

In this article, there are two meanings for the word “command”: shell commands, we will refer to them with shell commands

interactive Emacs functions, we will refer to them with commands

Emacs Single Shell Command API

The main commands are:

function execution return value spawned buffers shell-command-to-string (command) synchronous stdout shell-command (command & output-buffer error-buffer) synchronous return code stdout and stderr async-shell-command (command & output-buffer error-buffer) asynchronous window containing output-buffer stdout and stderr

As you can see, the three of those only accept a limited number of arguments.

But more are supported implicitly via various variables.

Here is a list of the most relevant ones:

var description default-directory location from which to launch shell explicit-shell-file-name / shell-file-name shell interpreter exec (e.g. bash …) shell-command-switch switch argument to use for running a command

So, by redefining the value of those vars we can change the behavior of the functions.

Specifically:

by having the default-directory be on a remote server (via TRAMP), we can open a shell on this remote server

be on a remote server (via TRAMP), we can open a shell on this remote server by changing the other vars, we can spawn a shell from non-default interpreters (fish, zsh, ksh…)

To prevent having to redefine those values, we can let-bind those vars for the duration of the execution (dynamic binding).

( defun my/uname-local () ( interactive ) ( let (( default-directory "~" ) ( explicit-shell-file-name "fish" )) ( message "Launching \"uname -a\" locally" ) ( message ( shell-command-to-string "uname -a" )))) ( defun my/uname-on-raspi () ( interactive ) ( let (( default-directory "/ssh:pi@raspi:/~" ) ( explicit-shell-file-name "bash" )) ( message "Launching \"uname -a\" on Raspberry Pi" ) ( message ( shell-command-to-string "uname -a" ))))

That’s pretty cool, but I’m not too fond of this hidden part of API.

Making the Implicit Explicit

Thankfully, it’s pretty straightforward to map implicit parameters to explicit ones and define a helper function.

As we want those params to be optional, it’s more convenient define them as keywords.

Click to toggle ;; ------------------------------------------------------------------------ ;; VARS ( defvar prf-default-remote-shell-interpreter "/bin/bash" ) ( defvar prf-default-remote-shell-interpreter-args ' ( "-c" "export EMACS=; export TERM=dumb; stty echo; bash" )) ( defvar prf-default-remote-shell-interpreter-command-switch "-c" ) ;; ------------------------------------------------------------------------ ;; HELPER ( defun with-shell-interpreter--normalize-path ( path ) "Normalize path, converting \\ into /." ( subst-char-in-string ?\\ ?/ path )) ( defun with-shell-interpreter--get-interpreter-name ( interpreter ) ( file-name-nondirectory interpreter )) ;; ------------------------------------------------------------------------ ;; MAIN ( cl-defun eval-with-shell-interpreter ( &key form path interpreter interpreter-args command-switch ) ( unless path ( setq path default-directory )) ( unless ( file-exists-p path ) ( error "Path %s doesn't seem to exist" path )) ( let* (( func ( if ( functionp form ) form ;; Try to use the "current" lexical/dynamic mode for `form'. ( eval ` ( lambda () , form ) lexical-binding ))) ( is-remote ( file-remote-p path )) ( interpreter ( or interpreter ( if is-remote prf-default-remote-shell-interpreter shell-file-name ))) ( interpreter ( with-shell-interpreter--normalize-path interpreter )) ( interpreter-name ( with-shell-interpreter--get-interpreter-name interpreter )) ( explicit-interpreter-args-var ( intern ( concat "explicit-" interpreter-name "-args" ))) ( interpreter-args ( or interpreter-args ( when is-remote prf-default-remote-shell-interpreter-args ))) ( command-switch ( or command-switch ( if is-remote prf-default-remote-shell-interpreter-command-switch shell-command-switch ))) ( default-directory path ) ( shell-file-name interpreter ) ( explicit-shell-file-name interpreter ) ( shell-command-switch command-switch )) ( cl-progv ( list explicit-interpreter-args-var ) ( list ( or interpreter-args ( when ( boundp explicit-interpreter-args-var ) ( symbol-value explicit-interpreter-args-var )))) ( funcall func ))))

Note that we are defining prf-default-remote-shell-interpreter to have a default interpreter different from local shell-file-name .

This allows rewriting the my/uname-local example with:

( defun my/uname-local () ( interactive ) ( eval-with-shell-interpreter :path "~" :interpreter "fish" :form ' ( progn ( message "Launching \"uname -a\" locally" ) ( message ( shell-command-to-string "uname -a" )))))

That’s pretty cool, but having to quote :form and wrap it in a progn is kinda cumbersome.

A macro wrapper to the rescue:

( defmacro with-shell-interpreter ( &rest args ) ( declare ( indent 1 ) ( debug t )) ` ( eval-with-shell-interpreter :form ( lambda () , ( cons 'progn ( with-shell-interpreter--plist-get args :form ))) :path , ( plist-get args :path ) :interpreter , ( plist-get args :interpreter ) :interpreter-args , ( plist-get args :interpreter-args ) :command-switch , ( plist-get args :command-switch ))) ( defun with-shell-interpreter--plist-get ( plist prop ) "Like `plist-get' except allows value to be multiple elements." ( unless ( null plist ) ( cl-loop with passed = nil for e in plist until ( and passed ( keywordp e ) ( not ( eq e prop ))) if ( and passed ( not ( keywordp e ))) collect e else if ( not passed ) do ( setq passed 't ))))

Which allows us to rewrite it like so:

( defun my/uname-local () ( interactive ) ( with-shell-interpreter :path "~" :interpreter "fish" :form ( message "Launching \"uname -a\" locally" ) ( message ( shell-command-to-string "uname -a" ))))

The code for with-shell-interpreter can be found in package with-shell-interpreter.

Even better

Let’s just spin off our own version of shell-command-to-string .

( cl-defun friendly-shell-command-to-string ( command &key path interpreter command-switch ) "Call CMD w/ `shell-command-to-string' on host and location described by PATH" ( with-shell-interpreter :form ( shell-command-to-string command ) :path path :interpreter interpreter :command-switch command-switch ))

Our example command becomes:

( defun my/uname-local () ( interactive ) ( message "Launching \"uname -a\" locally" ) ( friendly-shell-command-to-string "uname -a" :path "~" :interpreter "fish" ))

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

Notes

Tagged #emacs.

Please enable JavaScript to view the comments powered by Disqus.