4

I would like to be able to manage external processes from Emacs, in a way that is:

  • (a) safe w.r.t. arguments escaping for the shell,
  • (b) efficient both for local and remote processes (using TRAMP w/ ssh).

Here are two solutions I can think of to implement calling a possibly remote process:

  1. using shell-command: this has the advantage that it is fast (meets criterion b), but one needs to pass a full shell command (fails for criterion a).

  2. using start-file-process as recommended by the TRAMP documentation: this is nice in terms of arguments handling (criterion a), but a new SSH connection seems to be called for each process, which dramatically slows things down in remote configurations (criterion b).

NB: these solutions also differ in that (1) is synchronous whereas (2) is asynchronous, but I don't mention it because (a)synchronicity doesn't matter for my application.

It looks like option (2) is slow because it spawns a new ssh process, whereas option (1) reuses an already existing one. Is there any high-level function allowing to call a possibly remote process, while still being efficient by reusing an existing connection if possible?


Below is the code I used to benchmark both solutions:

(let ((test/remote-buffer (find-file "/xxx.xxx.xxx.xxx:/home/LOGIN/"))
      (test/output-buffer (get-buffer-create "*hostname*")))
  (with-current-buffer test/output-buffer
    (erase-buffer))

  (with-current-buffer test/remote-buffer
    (setq test/begin (current-time))
    (shell-command "hostname" test/output-buffer)
    (message "1-Elapsed: %f" (float-time (time-subtract (current-time) test/begin))))

  (with-current-buffer test/remote-buffer
    (setq test/begin (current-time))
    (set-process-sentinel
     (start-file-process "hostname" test/output-buffer "hostname")
     (lambda (p e)
       (message "2-Elapsed: %f" (float-time (time-subtract (current-time) test/begin)))))))

yielding output:

1-Elapsed: 0.010178
Tramp: Opening connection for xxx.xxx.xxx.xxx using scp...
Tramp: Sending command `exec ssh -q   -o ControlPath=/tmp/tramp.12155IuC.%r@%h:%p -o ControlMaster=auto -e none xxx.xxx.xxx.xxx'
Tramp: Waiting for prompts from remote shell...done
Tramp: Found remote shell prompt on `xxx.xxx.xxx.xxx'
Tramp: Opening connection for xxx.xxx.xxx.xxx using scp...done
2-Elapsed: 1.116449

In the case of local processes, both implementations take a similar elapsed time, which is negligible (and often faster for solution 2).

François Févotte
  • 5,917
  • 1
  • 24
  • 37

1 Answers1

5

What about writing a "safe" version of shell-command that would properly escape arguments using shell-quote-argument ?

For example:

(defun safe-shell-command (args &optional output-buffer error-buffer)
  (apply 'shell-command (mapconcat 'shell-quote-argument args " ")
         output-buffer error-buffer nil))

(the same would easily apply to async-shell-command)

Obviously it has the same performance characteristics as shell-command

The manual has a section on shell arguments. It also mentions combine-and-quote-strings, which might be useful depending on the case: it produces "less quoted" arguments, for example $ characters are left alone.

Sigma
  • 4,510
  • 21
  • 27
  • Thanks, this does the job nicely and simply. I have to admit I'm still a bit disappointed though: I feel like quoting and concatenating arguments into a string which will then be parsed by the shell is an unnecessary burden... – François Févotte Sep 29 '14 at 18:57