Skip to content

p3r7/with-shell-interpreter

Repository files navigation

with-shell-interpreter MELPA MELPA Stable

Helper for Emacs shell command APIs, making implicit argument as explicit keyword arguments.

This package is a library and does not provide any command.

It is inspired by the eval-after-load / with-eval-after-load functions.

It does the heavy lifting behind packages friendly-shell-command and friendly-shell.

For more context, read the accompanying blog post.

Installation

The package is available on Melpa.

With use-package:

(use-package with-shell-interpreter)

Manually:

M-x package-install with-shell-interpreter

Usage

We recommend using the macro with-shell-interpreter. It's a more convenient version of with-shell-interpreter-eval that prevents having to quote :form and wrap it in a progn.

keyword argument implicit var being let-bound mandatory? description
:form n/a ✔️ The s-expressions to eval.
:path default-directory The path from which to eval.
:interpreter explicit-shell-file-name / shell-file-name Name or absolute path of shell interpreter executable.
:interpreter-args explicit-INTEPRETER-args Login args to call interpreter with for login.
:command-switch shell-command-switch Command switch arg for asking interpreter to run a shell command.
:w32-arg-quote w32-quote-process-args Character to use for quoting shell arguments (only on the Windows build of Emacs)
:allow-local-vars n/a Whether to allow buffer-local and/or connection-local values

:form is expected to contain calls to functions relying on the Emacs shell APIs (e.g. shell, shell-command, async-shell-command and shell-command-to-string).

Setting :path to a remote location (with TRAMP format, i.e. /<method>:<user>@<host>:<localname>) allows running form with interpreter of remote server.

:interpreter-args is only usefull for interactive shells (from package shell-mode).

:command-switch is only usefull for single shell commands (from package simple).

:allow-local-vars can take the following values:

  • 'buffer: allow buffer-local vars values_
  • 'connection: allow connection-local values
  • 'both: allow both types of local values
  • 'none: ignore all local values

If left empty, here are the default values being used:

keyword argument fallback value (local path) fallback value (remote path)
:path current default-directory current default-directory
:interpreter shell-file-name with-shell-interpreter-default-remote
:interpreter-args explicit-INTEPRETER-args if set with-shell-interpreter-default-remote-args
:command-switch shell-command-switch with-shell-interpreter-default-remote-command-switch
:w32-arg-quote w32-quote-process-args w32-quote-process-args
:allow-local-vars nil 'connection

Examples

Getting the temperature from a Raspberry Pi:

(with-shell-interpreter
   :path "/ssh:pi@raspberry:/~"
   :interpreter "bash"
   :form
   (shell-command-to-string "vcgencmd measure_temp"))

Under Microsoft Windows, launching an interactive shell with the git-bash interpreter:

(with-shell-interpreter
  :path "~"                            ; ensure local path
  :interpreter "C:/Program Files/Git/bin/bash.exe"
  :form
  (let (current-prefix-arg '(4))       ; don't prompt user for interpreter
    (shell)))

For more practical examples, have a look at packages friendly-shell-command and friendly-shell (examples).

Configuration

Per connection default configuration

We generally want to have different values for remote connection as the user might have redefined the value of shell-file-name with something exotic (e.g. zsh) and we would want a safer default for remote servers.

Furthermore, under Microsoft Windows, shell-file-name defaults to cmdproxy.exe which is OK for local shells but sucks for remote ones...

We can of course use the :interpreter, :interpreter-args and :command-switch keyword parameters but it's cumbersome to define a function for each remote connection with a custom configuration.

Instead, the preferred way is to rely on connection-local variables.

with-shell-interpreter comes by default with its own custom implementation of connection-local variables. It can be configured with var with-shell-interpreter-connection-local-vars.

It comes with a fallback entry for default configuration of remote interpreters:

'((".*" . ((explicit-shell-file-name . "/bin/bash")
           (explicit-bash-args . ("-c" "export EMACS=; export TERM=dumb; stty echo; bash"))
           (shell-command-switch . "-c"))))

To add an entry, just:

(add-to-list with-shell-interpreter-connection-local-vars '(".*@openwrt") . ((explicit-shell-file-name . "/bin/ash")
                                                                             (explicit-bash-args . ("-i"))
                                                                             (shell-command-switch . "-c")))

Alternatively, you can instead choose to use Emacs' native implementation (more restrictive and cumbersome to configure):

(setq with-shell-interpreter-connection-local-vars-implem 'native)

The native implementation is only available since Emacs 26.1. Read docstrings of connection-local-set-profiles and connection-local-set-profile-variables for more details about it.

Default remote user

You might want to change the value of tramp-default-user if you usually connect to remote host with a user different than your local one.

Legibility

This code uses form feeds (^L character) as separators.

Either package form-feed or page-break-lines makes them appear as intended.

Package lisp-extra-font-lock is also recommended to distinguish between local and global vars in let expressions.