Make Eshell's "which" command extensible
Since 'eshell-named-command-hook' already makes execution of commands extensible, "which" should be too. This makes sure that "which" returns the right result for quoted commands like "/:cat". * lisp/eshell/em-alias.el (eshell-aliases-file): Allow it to be nil. (eshell-read-aliases-list, eshell-write-aliases-list): Check if 'eshell-aliases-file' is nil. (eshell-maybe-replace-by-alias--which): New function... (eshell-maybe-replace-by-alias): ... use it. * lisp/eshell/esh-cmd.el (eshell-named-command-hook): Update docstring. (eshell/which): Make extensible. (eshell--find-plain-lisp-command, eshell-plain-command--which): New functions. (eshell-plain-command): Use 'eshell--find-plain-lisp-command'. * lisp/eshell/esh-ext.el (eshell-explicit-command--which): New function... (eshell-explicit-command): ... unise it. (eshell-quoted-file-command--which): New function... (eshell-quoted-file-command): ... use it. (eshell-external-command--which): New function. * test/lisp/eshell/esh-cmd-tests.el (esh-cmd-test/which/plain/eshell-builtin) (esh-cmd-test/which/plain/external-program) (esh-cmd-test/which/plain/not-found, esh-cmd-test/which/alias) (esh-cmd-test/which/explicit, esh-cmd-test/which/explicit/not-found) (esh-cmd-test/which/quoted-file) (esh-cmd-test/which/quoted-file/not-found): New tests. * test/lisp/eshell/eshell-tests-helpers.el (with-temp-eshell-settings): Don't load or save aliases. (eshell-command-result--match,eshell-command-result--match-explainer) (eshell-command-result-match): New functions.
This commit is contained in:
parent
6a0f4d333a
commit
1df3554f07
5 changed files with 173 additions and 66 deletions
|
@ -107,7 +107,9 @@ it will be written to this file. Thus, alias definitions (and
|
|||
deletions) are always permanent. This approach was chosen for the
|
||||
sake of simplicity, since that's pretty much the only benefit to be
|
||||
gained by using this module."
|
||||
:type 'file
|
||||
:version "30.1"
|
||||
:type '(choice (const :tag "Don't save aliases" nil)
|
||||
file)
|
||||
:group 'eshell-alias)
|
||||
|
||||
(defcustom eshell-bad-command-tolerance 3
|
||||
|
@ -186,29 +188,30 @@ file named by `eshell-aliases-file'.")
|
|||
"Read in an aliases list from `eshell-aliases-file'.
|
||||
This is useful after manually editing the contents of the file."
|
||||
(interactive)
|
||||
(let ((file eshell-aliases-file))
|
||||
(when (file-readable-p file)
|
||||
(setq eshell-command-aliases-list
|
||||
(with-temp-buffer
|
||||
(let (eshell-command-aliases-list)
|
||||
(insert-file-contents file)
|
||||
(while (not (eobp))
|
||||
(if (re-search-forward
|
||||
"^alias\\s-+\\(\\S-+\\)\\s-+\\(.+\\)")
|
||||
(setq eshell-command-aliases-list
|
||||
(cons (list (match-string 1)
|
||||
(match-string 2))
|
||||
eshell-command-aliases-list)))
|
||||
(forward-line 1))
|
||||
eshell-command-aliases-list))))))
|
||||
(when (and eshell-aliases-file
|
||||
(file-readable-p eshell-aliases-file))
|
||||
(setq eshell-command-aliases-list
|
||||
(with-temp-buffer
|
||||
(let (eshell-command-aliases-list)
|
||||
(insert-file-contents eshell-aliases-file)
|
||||
(while (not (eobp))
|
||||
(if (re-search-forward
|
||||
"^alias\\s-+\\(\\S-+\\)\\s-+\\(.+\\)")
|
||||
(setq eshell-command-aliases-list
|
||||
(cons (list (match-string 1)
|
||||
(match-string 2))
|
||||
eshell-command-aliases-list)))
|
||||
(forward-line 1))
|
||||
eshell-command-aliases-list)))))
|
||||
|
||||
(defun eshell-write-aliases-list ()
|
||||
"Write out the current aliases into `eshell-aliases-file'."
|
||||
(if (file-writable-p (file-name-directory eshell-aliases-file))
|
||||
(let ((eshell-current-handles
|
||||
(eshell-create-handles eshell-aliases-file 'overwrite)))
|
||||
(eshell/alias)
|
||||
(eshell-close-handles 0 'nil))))
|
||||
(when (and eshell-aliases-file
|
||||
(file-writable-p (file-name-directory eshell-aliases-file)))
|
||||
(let ((eshell-current-handles
|
||||
(eshell-create-handles eshell-aliases-file 'overwrite)))
|
||||
(eshell/alias)
|
||||
(eshell-close-handles 0 'nil))))
|
||||
|
||||
(defsubst eshell-lookup-alias (name)
|
||||
"Check whether NAME is aliased. Return the alias if there is one."
|
||||
|
@ -216,18 +219,26 @@ This is useful after manually editing the contents of the file."
|
|||
|
||||
(defvar eshell-prevent-alias-expansion nil)
|
||||
|
||||
(defun eshell-maybe-replace-by-alias--which (command)
|
||||
(unless (and eshell-prevent-alias-expansion
|
||||
(member command eshell-prevent-alias-expansion))
|
||||
(when-let ((alias (eshell-lookup-alias command)))
|
||||
(concat command " is an alias, defined as \"" (cadr alias) "\""))))
|
||||
|
||||
(defun eshell-maybe-replace-by-alias (command _args)
|
||||
"Call COMMAND's alias definition, if it exists."
|
||||
(unless (and eshell-prevent-alias-expansion
|
||||
(member command eshell-prevent-alias-expansion))
|
||||
(let ((alias (eshell-lookup-alias command)))
|
||||
(if alias
|
||||
(throw 'eshell-replace-command
|
||||
`(let ((eshell-command-name ',eshell-last-command-name)
|
||||
(eshell-command-arguments ',eshell-last-arguments)
|
||||
(eshell-prevent-alias-expansion
|
||||
',(cons command eshell-prevent-alias-expansion)))
|
||||
,(eshell-parse-command (nth 1 alias))))))))
|
||||
(when-let ((alias (eshell-lookup-alias command)))
|
||||
(throw 'eshell-replace-command
|
||||
`(let ((eshell-command-name ',eshell-last-command-name)
|
||||
(eshell-command-arguments ',eshell-last-arguments)
|
||||
(eshell-prevent-alias-expansion
|
||||
',(cons command eshell-prevent-alias-expansion)))
|
||||
,(eshell-parse-command (nth 1 alias)))))))
|
||||
|
||||
(put 'eshell-maybe-replace-by-alias 'eshell-which-function
|
||||
#'eshell-maybe-replace-by-alias--which)
|
||||
|
||||
(defun eshell-alias-completions (name)
|
||||
"Find all possible completions for NAME.
|
||||
|
|
|
@ -154,7 +154,8 @@ To prevent a command from executing at all, set
|
|||
:type 'hook)
|
||||
|
||||
(defcustom eshell-named-command-hook nil
|
||||
"A set of functions called before a named command is invoked.
|
||||
"A set of functions called before
|
||||
a named command is invoked.
|
||||
Each function will be passed the command name and arguments that were
|
||||
passed to `eshell-named-command'.
|
||||
|
||||
|
@ -173,7 +174,12 @@ For example:
|
|||
|
||||
Although useless, the above code will cause any non-glob, non-Lisp
|
||||
command (i.e., `ls' as opposed to `*ls' or `(ls)') to be replaced by a
|
||||
call to `cd' using the arguments that were passed to the function."
|
||||
call to `cd' using the arguments that were passed to the function.
|
||||
|
||||
When adding a function to this hook, you should also set the property
|
||||
`eshell-which-function' for the function. This property should hold a
|
||||
function that takes a single COMMAND argument and returns a string
|
||||
describing where Eshell will find the function."
|
||||
:type 'hook)
|
||||
|
||||
(defcustom eshell-pre-rewrite-command-hook
|
||||
|
@ -1299,34 +1305,18 @@ have been replaced by constants."
|
|||
(defun eshell/which (command &rest names)
|
||||
"Identify the COMMAND, and where it is located."
|
||||
(dolist (name (cons command names))
|
||||
(let (program alias direct)
|
||||
(if (eq (aref name 0) eshell-explicit-command-char)
|
||||
(setq name (substring name 1)
|
||||
direct t))
|
||||
(if (and (not direct)
|
||||
(fboundp 'eshell-lookup-alias)
|
||||
(setq alias
|
||||
(eshell-lookup-alias name)))
|
||||
(setq program
|
||||
(concat name " is an alias, defined as \""
|
||||
(cadr alias) "\"")))
|
||||
(unless program
|
||||
(setq program
|
||||
(let* ((esym (eshell-find-alias-function name))
|
||||
(sym (or esym (intern-soft name))))
|
||||
(if (and (or esym (and sym (fboundp sym)))
|
||||
(or eshell-prefer-lisp-functions (not direct)))
|
||||
(or (with-output-to-string
|
||||
(require 'help-fns)
|
||||
(princ (format "%s is " sym))
|
||||
(help-fns-function-description-header sym))
|
||||
name)
|
||||
(eshell-search-path name)))))
|
||||
(if (not program)
|
||||
(eshell-error (format "which: no %s in (%s)\n"
|
||||
name (string-join (eshell-get-path t)
|
||||
(path-separator))))
|
||||
(eshell-printn program)))))
|
||||
(condition-case error
|
||||
(eshell-printn
|
||||
(catch 'found
|
||||
(run-hook-wrapped
|
||||
'eshell-named-command-hook
|
||||
(lambda (hook)
|
||||
(when-let (((symbolp hook))
|
||||
(which-func (get hook 'eshell-which-function))
|
||||
(result (funcall which-func command)))
|
||||
(throw 'found result))))
|
||||
(eshell-plain-command--which name)))
|
||||
(error (eshell-error (format "which: %s\n" (cadr error)))))))
|
||||
|
||||
(put 'eshell/which 'eshell-no-numeric-conversions t)
|
||||
|
||||
|
@ -1376,17 +1366,31 @@ COMMAND may result in an alias being executed, or a plain command."
|
|||
(if (functionp sym)
|
||||
sym))))
|
||||
|
||||
(defun eshell--find-plain-lisp-command (command)
|
||||
"Look for `eshell/COMMAND' and return it when COMMAND should use it."
|
||||
(let* ((esym (eshell-find-alias-function command))
|
||||
(sym (or esym (intern-soft command))))
|
||||
(when (and sym (fboundp sym)
|
||||
(or esym eshell-prefer-lisp-functions
|
||||
(not (eshell-search-path command))))
|
||||
sym)))
|
||||
|
||||
(defun eshell-plain-command--which (command)
|
||||
(if-let ((sym (eshell--find-plain-lisp-command command)))
|
||||
(or (with-output-to-string
|
||||
(require 'help-fns)
|
||||
(princ (format "%s is " sym))
|
||||
(help-fns-function-description-header sym))
|
||||
command)
|
||||
(eshell-external-command--which command)))
|
||||
|
||||
(defun eshell-plain-command (command args)
|
||||
"Insert output from a plain COMMAND, using ARGS.
|
||||
COMMAND may result in either a Lisp function being executed by name,
|
||||
or an external command."
|
||||
(let* ((esym (eshell-find-alias-function command))
|
||||
(sym (or esym (intern-soft command))))
|
||||
(if (and sym (fboundp sym)
|
||||
(or esym eshell-prefer-lisp-functions
|
||||
(not (eshell-search-path command))))
|
||||
(eshell-lisp-command sym args)
|
||||
(eshell-external-command command args))))
|
||||
(if-let ((sym (eshell--find-plain-lisp-command command)))
|
||||
(eshell-lisp-command sym args)
|
||||
(eshell-external-command command args)))
|
||||
|
||||
(defun eshell-exec-lisp (printer errprint func-or-form args form-p)
|
||||
"Execute a Lisp FUNC-OR-FORM, maybe passing ARGS.
|
||||
|
|
|
@ -182,6 +182,11 @@ commands on your local host by using the \"/local:\" prefix, like
|
|||
(add-hook 'eshell-named-command-hook #'eshell-quoted-file-command nil t)
|
||||
(add-hook 'eshell-named-command-hook #'eshell-explicit-command nil t))
|
||||
|
||||
(defun eshell-explicit-command--which (command)
|
||||
(when (and (> (length command) 1)
|
||||
(eq (aref command 0) eshell-explicit-command-char))
|
||||
(eshell-external-command--which (substring command 1))))
|
||||
|
||||
(defun eshell-explicit-command (command args)
|
||||
"If a command name begins with \"*\", always call it externally.
|
||||
This bypasses all Lisp functions and aliases."
|
||||
|
@ -194,6 +199,13 @@ This bypasses all Lisp functions and aliases."
|
|||
(error "%s: external command not found"
|
||||
(substring command 1))))))
|
||||
|
||||
(put 'eshell-explicit-command 'eshell-which-function
|
||||
#'eshell-explicit-command--which)
|
||||
|
||||
(defun eshell-quoted-file-command--which (command)
|
||||
(when (file-name-quoted-p command)
|
||||
(eshell-external-command--which (file-name-unquote command))))
|
||||
|
||||
(defun eshell-quoted-file-command (command args)
|
||||
"If a command name begins with \"/:\", always call it externally.
|
||||
Similar to `eshell-explicit-command', this bypasses all Lisp functions
|
||||
|
@ -201,6 +213,9 @@ and aliases, but it also ignores file name handlers."
|
|||
(when (file-name-quoted-p command)
|
||||
(eshell-external-command (file-name-unquote command) args)))
|
||||
|
||||
(put 'eshell-quoted-file-command 'eshell-which-function
|
||||
#'eshell-quoted-file-command--which)
|
||||
|
||||
(defun eshell-remote-command (command args)
|
||||
"Insert output from a remote COMMAND, using ARGS.
|
||||
A \"remote\" command in Eshell is something that executes on a different
|
||||
|
@ -239,6 +254,11 @@ current working directory."
|
|||
(eshell-gather-process-output
|
||||
(car interp) (append (cdr interp) args)))))
|
||||
|
||||
(defun eshell-external-command--which (command)
|
||||
(or (eshell-search-path command)
|
||||
(error "no %s in (%s)" command
|
||||
(string-join (eshell-get-path t) (path-separator)))))
|
||||
|
||||
(defun eshell-external-command (command args)
|
||||
"Insert output from an external COMMAND, using ARGS."
|
||||
(cond
|
||||
|
|
|
@ -517,4 +517,50 @@ NAME is the name of the test case."
|
|||
;; Make sure we can call another command after throwing.
|
||||
(eshell-match-command-output "echo again" "\\`again\n")))
|
||||
|
||||
|
||||
;; `which' command
|
||||
|
||||
(ert-deftest esh-cmd-test/which/plain/eshell-builtin ()
|
||||
"Check that `which' finds Eshell built-in functions."
|
||||
(eshell-command-result-match "which cat" "\\`eshell/cat"))
|
||||
|
||||
(ert-deftest esh-cmd-test/which/plain/external-program ()
|
||||
"Check that `which' finds external programs."
|
||||
(skip-unless (executable-find "sh"))
|
||||
(eshell-command-result-equal "which sh"
|
||||
(concat (executable-find "sh") "\n")))
|
||||
|
||||
(ert-deftest esh-cmd-test/which/plain/not-found ()
|
||||
"Check that `which' reports an error for not-found commands."
|
||||
(skip-when (executable-find "nonexist"))
|
||||
(eshell-command-result-match "which nonexist" "\\`which: no nonexist in"))
|
||||
|
||||
(ert-deftest esh-cmd-test/which/alias ()
|
||||
"Check that `which' finds aliases."
|
||||
(with-temp-eshell
|
||||
(eshell-insert-command "alias cat '*cat $@*'")
|
||||
(eshell-match-command-output "which cat" "\\`cat is an alias")))
|
||||
|
||||
(ert-deftest esh-cmd-test/which/explicit ()
|
||||
"Check that `which' finds explicitly-external programs."
|
||||
(skip-unless (executable-find "cat"))
|
||||
(eshell-command-result-match "which *cat"
|
||||
(concat (executable-find "cat") "\n")))
|
||||
|
||||
(ert-deftest esh-cmd-test/which/explicit/not-found ()
|
||||
"Check that `which' reports an error for not-found explicit commands."
|
||||
(skip-when (executable-find "nonexist"))
|
||||
(eshell-command-result-match "which *nonexist" "\\`which: no nonexist in"))
|
||||
|
||||
(ert-deftest esh-cmd-test/which/quoted-file ()
|
||||
"Check that `which' finds programs with quoted file names."
|
||||
(skip-unless (executable-find "cat"))
|
||||
(eshell-command-result-match "which /:cat"
|
||||
(concat (executable-find "cat") "\n")))
|
||||
|
||||
(ert-deftest esh-cmd-test/which/quoted-file/not-found ()
|
||||
"Check that `which' reports an error for not-found quoted commands."
|
||||
(skip-when (executable-find "nonexist"))
|
||||
(eshell-command-result-match "which /:nonexist" "\\`which: no nonexist in"))
|
||||
|
||||
;; esh-cmd-tests.el ends here
|
||||
|
|
|
@ -30,6 +30,8 @@
|
|||
(require 'esh-mode)
|
||||
(require 'eshell)
|
||||
|
||||
(defvar eshell-aliases-file nil)
|
||||
(defvar eshell-command-aliases-list nil)
|
||||
(defvar eshell-history-file-name nil)
|
||||
(defvar eshell-last-dir-ring-file-name nil)
|
||||
|
||||
|
@ -60,6 +62,8 @@ beginning of the test file."
|
|||
;; just enable this selectively when needed.) See also
|
||||
;; `eshell-test-command-result' below.
|
||||
(eshell-debug-command (cons 'process eshell-debug-command))
|
||||
(eshell-aliases-file nil)
|
||||
(eshell-command-aliases-list nil)
|
||||
(eshell-history-file-name nil)
|
||||
(eshell-last-dir-ring-file-name nil)
|
||||
(eshell-module-loading-messages nil))
|
||||
|
@ -196,6 +200,28 @@ inserting the command."
|
|||
(eshell-test-command-result command)
|
||||
result)))))
|
||||
|
||||
(defun eshell-command-result--match (_command regexp actual)
|
||||
"Compare the ACTUAL result of a COMMAND with REGEXP."
|
||||
(string-match regexp actual))
|
||||
|
||||
(defun eshell-command-result--match-explainer (command regexp actual)
|
||||
"Explain the result of `eshell-command-result--match'."
|
||||
`(mismatched-result
|
||||
(command ,command)
|
||||
(result ,actual)
|
||||
(regexp ,regexp)))
|
||||
|
||||
(put 'eshell-command-result--match 'ert-explainer
|
||||
#'eshell-command-result--match-explainer)
|
||||
|
||||
(defun eshell-command-result-match (command regexp)
|
||||
"Execute COMMAND non-interactively and compare it to REGEXP."
|
||||
(ert-info (#'eshell-get-debug-logs :prefix "Command logs: ")
|
||||
(let ((eshell-module-loading-messages nil))
|
||||
(should (eshell-command-result--match
|
||||
command regexp
|
||||
(eshell-test-command-result command))))))
|
||||
|
||||
(provide 'eshell-tests-helpers)
|
||||
|
||||
;;; eshell-tests-helpers.el ends here
|
||||
|
|
Loading…
Add table
Reference in a new issue