Being able to automate things with Emacs can be painfully hard at times, and ridiculously easy, once we get our hands on the right tool.
Let's illustrate this by solving the following problem with Emacs.
How to run a command on a remote server?
There are many commands, in Emacs, for running a subprocess or an inferior shell. For example
- process-file
- start-file-process
- start-process
- shell-command
- shell
- …
In a following article, I plan to cover all of these commands to show in what they differ.
But for now, we are interested in shell-command
, or more specifically shell-command-to-string
, because we may be interested in the output of the program, to be used downstream.
Run a command on a remote server
Let's assume we have a remote machine wisefully called remote
, and BASH is the default shell on that machine.
In the following example, we run the command echo on the server remote
.
(let ((default-directory (expand-file-name "/ssh:remote:~/")))
(with-connection-local-variables
(shell-command-to-string "echo Hi! I am on $HOSTNAME")
))
Hi! I am on remote
You can pass any bash script to shell-command-to-string
and it will return the output as a string.
If you tried this, and only Hi! I am on .
appears, then you likely need to set your shell to BASH. Let's see how to run the shell we want.
Change the shell used for the remote command
The variable shell-file-name
holds the path to the shell used by commands like shell
, shell-command
, shell-command-to-string
. So we have to change it.
But since we run the command on a remote server through SSH, our local value will not affect the value of shell-file-name
used on the remote. Also, wrapping our previous code in a let
statement to redefine the variable will not work, because we are only changing the globally scoped variable, not the one used by TRAMP to make the SSH connection.
Thanks to the recently introduced connection-local variables, we can set variables to be used by TRAMP to the values we want, on specific connections. It gives a great amount of flexibility. Here is how.
(connection-local-set-profile-variables
'remote-fish
'((shell-file-name . "/bin/fish")
(shell-command-switch . "-c")
(shell-interactive-switch . "-i")
(shell-login-switch . "-l")))
(connection-local-set-profiles
'(:application tramp :protocol "ssh" :machine "remote")
'remote-fish)
(let ((default-directory (expand-file-name "/ssh:remote:~/")))
(with-connection-local-variables
(shell-command-to-string "echo Hi! I am on $hostname")
))
Hi! I am on remote
We were able to print the value of $hostname
which would not be defined in ZSH or BASH, because we have set remote-fish
to hold the local variables we needed.
The setting was in three steps.
- We create new profile called
remote-fish
, which provides an association list of variables and their respective value. This is done with the functionconnection-local-set-profile-variables
. - We set the newly created profile to match a certain connection by tramp. This is done by the function
connection-local-set-profile
. You can actually specify for which protocol and which machine this profile should be used. - We finally wrap the command
shell-command-to-string
inside awith-connection-local-variables
statement.
Let's step up again and see how to change environment variables.
Change environment variables
The environment variables that a process ran from Emacs will see are defined by the variable process-environment
.
This time, there is no trick, and we can just set this variable in the let
statement like so.
(let ((default-directory (expand-file-name "/ssh:remote:~/"))
(process-environment '("MYVAR=foobar")))
(with-connection-local-variables
(shell-command-to-string "echo Hi! I am on $HOSTNAME. Also MYVAR is set to $MYVAR.")
))
Hi! I am on remote. Also MYVAR is set to foobar.
We just use our profile set for remote
by wrapping the shell command call inside with-connection-local-variables
.
That's all!
Thank you for reading :) Adam.