]> git.eshelyaron.com Git - emacs.git/commitdiff
Make Eshell's "which" command extensible
authorJim Porter <jporterbugs@gmail.com>
Thu, 30 May 2024 02:21:09 +0000 (19:21 -0700)
committerEshel Yaron <me@eshelyaron.com>
Thu, 30 May 2024 14:28:38 +0000 (16:28 +0200)
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.

(cherry picked from commit 1df3554f0766c91a9452cd40f74f469ed612eda6)

lisp/eshell/em-alias.el
lisp/eshell/esh-cmd.el
lisp/eshell/esh-ext.el
test/lisp/eshell/esh-cmd-tests.el
test/lisp/eshell/eshell-tests-helpers.el

index d12b382d885e79c24ff60f7222058d671c22cc24..ff0620702cf3a5b44c707ee987f6838c03b2abba 100644 (file)
@@ -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.
index a093b7face98ea20eef7a505dab4e504c294b6f5..ee9143db765558ad4d828429f8335ade7852db0b 100644 (file)
@@ -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.
index df8f71989172194d7dd2a17f7b54be6c50eb29d1..3c4deb32601ae2f089f79a5366c2ecc64f3db2f7 100644 (file)
@@ -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
index d84f8802bdc3bf04e2754b4015928bd28115f2f1..4d27f6db2ee447a9354970cd8a7ed5729138ae3a 100644 (file)
@@ -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")))
 
+\f
+;; `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
index a15fe61167659113a345a96c419d2cc0ab5995bc..bfd829c95e9640e9470973934456ae1b0a7b4266 100644 (file)
@@ -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