If @var{connected} is non-@code{nil}, this function returns @code{nil}
even if @var{filename} is remote, if Emacs has no network connection
to its host. This is useful when you want to avoid the delay of
-making connections when they don't exist.
+making connections when they don't exist. If @var{connected} is
+@code{never}, @emph{never} use an existing connection to return the
+identification, even if one is already present (this is otherwise like
+a value of @code{nil}). This lets you prevent any connection-specific
+logic, such as expanding the local part of the file name.
@end defun
@defun unhandled-file-name-directory filename
this behavior annoying, you can enable the optional electric forward
slash module (@pxref{Electric forward slash}).
+@vindex eshell-explicit-remote-commands
+When running commands, you can also make them explicitly remote by
+prefixing the command name with a remote identifier, e.g.@:
+@samp{/ssh:user@@remote:whoami}. This runs the command @code{whoami}
+over the SSH connection for @code{user@@remote}, no matter your
+current directory. If you want to explicitly run a @emph{local}
+command even when in a remote directory, you can prefix the command
+name with @kbd{/:}, like @samp{/:whoami}. In either case, you can
+also specify the absolute path to the program, e.g.@:
+@samp{/ssh:user@@remote:/usr/bin/whoami}. To disable this syntax, set
+the option @code{eshell-explicit-remote-commands} to @code{nil}.
+
@node History
@section History
@cmindex history
more information, see the "(eshell) Dollars Expansion" node in the
Eshell manual.
++++
+*** Eshell commands can now be explicitly-remote (or local).
+By prefixing a command name in Eshell with a remote identifier, like
+"/ssh:user@remote:whoami", you can now runs commands on a particular
+host no matter your current directory. Likewise, you can run a
+command on your local system no matter your current directory via
+"/:whoami". For more information, see the "(eshell) Remote Access"
+node in the Eshell manual.
+
+++
*** Eshell's '$UID' and '$GID' variables are now connection-aware.
Now, when expanding '$UID' or '$GID' in a remote directory, the value
:type 'character
:group 'eshell-ext)
+(defcustom eshell-explicit-remote-commands t
+ "If non-nil, support explicitly-remote commands.
+These are commands with a full remote file name, such as
+\"/ssh:host:whoami\". If this is enabled, you can also run
+explicitly-local commands by using a quoted file name, like
+\"/:whoami\"."
+ :type 'boolean
+ :group 'eshell-ext)
+
;;; Functions:
(defun eshell-ext-initialize () ;Called from `eshell-mode' via intern-soft!
"Initialize the external command handling code."
- (add-hook 'eshell-named-command-hook #'eshell-explicit-command nil t))
+ (add-hook 'eshell-named-command-hook #'eshell-explicit-command nil t)
+ (when eshell-explicit-remote-commands
+ (add-hook 'eshell-named-command-hook
+ #'eshell-handle-remote-command nil t)))
(defun eshell-explicit-command (command args)
"If a command name begins with `*', call it externally always.
(error "%s: external command not found"
(substring command 1))))))
+(defun eshell-handle-remote-command (command args)
+ "Handle remote (or quoted) COMMAND names, using ARGS.
+This calls the appropriate function for commands that aren't on
+the connection associated with `default-directory'. (See
+`eshell-explicit-remote-commands'.)"
+ (if (file-name-quoted-p command)
+ (let ((default-directory (if (file-remote-p default-directory)
+ (expand-file-name "~")
+ default-directory)))
+ (eshell-external-command (file-name-unquote command) args))
+ (when (file-remote-p command)
+ (eshell-remote-command command args))))
+
(defun eshell-remote-command (command args)
"Insert output from a remote COMMAND, using ARGS.
A remote command is something that executes on a different machine.
-An external command simply means external to Emacs.
-
-Note that this function is very crude at the moment. It gathers up
-all the output from the remote command, and sends it all at once,
-causing the user to wonder if anything's really going on..."
- (let ((outbuf (generate-new-buffer " *eshell remote output*"))
- (errbuf (generate-new-buffer " *eshell remote error*"))
- (command (file-local-name command))
- (exitcode 1))
- (unwind-protect
- (progn
- (setq exitcode
- (shell-command
- (mapconcat #'shell-quote-argument
- (append (list command) args) " ")
- outbuf errbuf))
- (eshell-print (with-current-buffer outbuf (buffer-string)))
- (eshell-error (with-current-buffer errbuf (buffer-string))))
- (eshell-close-handles exitcode 'nil)
- (kill-buffer outbuf)
- (kill-buffer errbuf))))
+An external command simply means external to Emacs."
+ (let* ((cwd-connection (file-remote-p default-directory))
+ (command-connection (file-remote-p command))
+ (default-directory (if (equal cwd-connection command-connection)
+ default-directory
+ command-connection))
+ ;; Never use the remote connection here. We don't want to
+ ;; expand the local name! Instead, we want it as the user
+ ;; typed, so that if COMMAND is "/ssh:host:cat", we just get
+ ;; "cat" as the result.
+ (command-localname (file-remote-p command 'localname 'never)))
+ (unless command-connection
+ (error "%s: not a remote command" command))
+ (eshell-external-command command-localname args)))
(defun eshell-external-command (command args)
"Insert output from an external COMMAND, using ARGS."
If CONNECTED is non-nil, return an identification only if FILE is
located on a remote system and a connection is established to
-that remote system.
+that remote system. If CONNECTED is `never', never use an
+existing connection to return the identification (this is
+otherwise like a value of nil).
Tip: You can use this expansion of remote identifier components
to derive a new remote file name from an existing one. For
(let ((tramp-verbose (min tramp-verbose 3)))
(when (tramp-tramp-file-p filename)
(let* ((o (tramp-dissect-file-name filename))
- (p (tramp-get-connection-process o))
+ (p (and (not (eq connected 'never))
+ (tramp-get-connection-process o)))
(c (and (process-live-p p)
(tramp-get-connection-property p "connected"))))
;; We expand the file name only, if there is already a connection.
(with-parsed-tramp-file-name
(if c (expand-file-name filename) filename) nil
- (and (or (not connected) c)
+ (and (or (memq connected '(nil never)) c)
(cond
((eq identification 'method) method)
;; Domain and port are appended to user and host,
;;; Code:
+(require 'tramp)
(require 'ert)
(require 'esh-mode)
(require 'esh-ext)
(eshell-match-command-output "echo $PATH"
(concat original-path "\n")))))
+(ert-deftest esh-ext-test/explicitly-remote-command ()
+ "Test that an explicitly-remote command is remote no matter the current dir."
+ (skip-unless (and (eshell-tests-remote-accessible-p)
+ (executable-find "sh")))
+ (dolist (default-directory (list default-directory
+ ert-remote-temporary-file-directory))
+ (dolist (cmd (list "sh" (executable-find "sh")))
+ (ert-info ((format "Directory: %s; executable: %s" default-directory cmd))
+ (with-temp-eshell
+ ;; Check the value of $INSIDE_EMACS using `sh' in order to
+ ;; delay variable expansion.
+ (eshell-match-command-output
+ (format "%s%s -c 'echo $INSIDE_EMACS'"
+ (file-remote-p ert-remote-temporary-file-directory) cmd)
+ "eshell,tramp"))))))
+
+(ert-deftest esh-ext-test/explicitly-local-command ()
+ "Test that an explicitly-local command is local no matter the current dir."
+ (skip-unless (and (eshell-tests-remote-accessible-p)
+ (executable-find "sh")))
+ (dolist (default-directory (list default-directory
+ ert-remote-temporary-file-directory))
+ (dolist (cmd (list "sh" (executable-find "sh")))
+ (ert-info ((format "In directory: %s" default-directory))
+ (with-temp-eshell
+ ;; Check the value of $INSIDE_EMACS using `sh' in order to
+ ;; delay variable expansion.
+ (eshell-match-command-output
+ (format "/:%s -c 'echo $INSIDE_EMACS'" cmd)
+ "eshell\n"))))))
+
;; esh-ext-tests.el ends here