X-Git-Url: https://code.delx.au/gnu-emacs/blobdiff_plain/f8aefe82d2bbca46b162c75b1217d8b4bd9ebb09..4cb3bfa09c6838a579fcf3dab9b7ea1135a2fb4b:/lisp/shell.el diff --git a/lisp/shell.el b/lisp/shell.el index b7f62efdb8..de811543ba 100644 --- a/lisp/shell.el +++ b/lisp/shell.el @@ -1,7 +1,6 @@ -;;; shell.el --- specialized comint.el for running the shell +;;; shell.el --- specialized comint.el for running the shell -*- lexical-binding: t -*- -;; Copyright (C) 1988, 1993, 1994, 1995, 1996, 1997, 2000, 2001, -;; 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010 Free Software Foundation, Inc. +;; Copyright (C) 1988, 1993-1997, 2000-2011 Free Software Foundation, Inc. ;; Author: Olin Shivers ;; Simon Marshall @@ -80,7 +79,7 @@ ;; Shell Mode Commands: ;; shell Fires up the shell process -;; tab comint-dynamic-complete Complete filename/command/history +;; tab completion-at-point Complete filename/command/history ;; m-? comint-dynamic-list-filename-completions ;; List completions in help buffer ;; m-c-f shell-forward-command Forward a shell command @@ -97,7 +96,9 @@ ;;; Code: +(eval-when-compile (require 'cl)) (require 'comint) +(require 'pcomplete) ;;; Customization and Buffer Variables @@ -152,12 +153,14 @@ This is a fine thing to set in your `.emacs' file." :type '(repeat (string :tag "Suffix")) :group 'shell) -(defvar shell-delimiter-argument-list '(?\| ?& ?< ?> ?\( ?\) ?\;) +(defcustom shell-delimiter-argument-list nil ; '(?\| ?& ?< ?> ?\( ?\) ?\;) "List of characters to recognize as separate arguments. This variable is used to initialize `comint-delimiter-argument-list' in the -shell buffer. The value may depend on the operating system or shell. - -This is a fine thing to set in your `.emacs' file.") +shell buffer. The value may depend on the operating system or shell." + :type '(choice (const nil) + (repeat :tag "List of characters" character)) + :version "24.1" ; changed to nil (bug#8027) + :group 'shell) (defvar shell-file-name-chars (if (memq system-type '(ms-dos windows-nt cygwin)) @@ -180,12 +183,14 @@ shell buffer. The value may depend on the operating system or shell. This is a fine thing to set in your `.emacs' file.") (defvar shell-dynamic-complete-functions - '(comint-replace-by-expanded-history - shell-dynamic-complete-environment-variable - shell-dynamic-complete-command - shell-replace-by-expanded-directory - shell-dynamic-complete-filename - comint-dynamic-complete-filename) + '(comint-c-a-p-replace-by-expanded-history + shell-environment-variable-completion + shell-command-completion + shell-c-a-p-replace-by-expanded-directory + pcomplete-completions-at-point + shell-filename-completion + ;; Not sure when this one would still be useful. --Stef + comint-filename-completion) "List of functions called to perform completion. This variable is used to initialize `comint-dynamic-complete-functions' in the shell buffer. @@ -311,7 +316,7 @@ This mirrors the optional behavior of tcsh (its autoexpand and histlit). If the value is `input', then the expansion is seen on input. If the value is `history', then the expansion is only when inserting into the buffer's input ring. See also `comint-magic-space' and -`comint-dynamic-complete'. +`comint-dynamic-complete-functions'. This variable supplies a default for `comint-input-autoexpand', for Shell mode only." @@ -338,7 +343,7 @@ Thus, this does not include the shell's current directory.") (let ((map (nconc (make-sparse-keymap) comint-mode-map))) (define-key map "\C-c\C-f" 'shell-forward-command) (define-key map "\C-c\C-b" 'shell-backward-command) - (define-key map "\t" 'comint-dynamic-complete) + (define-key map "\t" 'completion-at-point) (define-key map (kbd "M-RET") 'shell-resync-dirs) (define-key map "\M-?" 'comint-dynamic-list-filename-completions) (define-key map [menu-bar completion] @@ -367,6 +372,46 @@ Thus, this does not include the shell's current directory.") ;;; Basic Procedures +(defcustom shell-dir-cookie-re nil + "Regexp matching your prompt, including some part of the current directory. +If your prompt includes the current directory or the last few elements of it, +set this to a pattern that matches your prompt and whose subgroup 1 matches +the directory part of it. +This is used by `shell-dir-cookie-watcher' to try and use this info +to track your current directory. It can be used instead of or in addition +to `dirtrack-mode'." + :group 'shell + :type '(choice (const nil) regexp)) + + +(defun shell-completion-vars () + "Setup completion vars for `shell-mode' and `read-shell-command'." + (set (make-local-variable 'comint-completion-fignore) + shell-completion-fignore) + (set (make-local-variable 'comint-delimiter-argument-list) + shell-delimiter-argument-list) + (set (make-local-variable 'comint-file-name-chars) shell-file-name-chars) + (set (make-local-variable 'comint-file-name-quote-list) + shell-file-name-quote-list) + (set (make-local-variable 'comint-dynamic-complete-functions) + shell-dynamic-complete-functions) + (set (make-local-variable 'pcomplete-parse-arguments-function) + ;; FIXME: This function should be moved to shell.el. + #'pcomplete-parse-comint-arguments) + (set (make-local-variable 'pcomplete-termination-string) + (cond ((not comint-completion-addsuffix) "") + ((stringp comint-completion-addsuffix) + comint-completion-addsuffix) + ((not (consp comint-completion-addsuffix)) " ") + (t (cdr comint-completion-addsuffix)))) + ;; Don't use pcomplete's defaulting mechanism, rely on + ;; shell-dynamic-complete-functions instead. + (set (make-local-variable 'pcomplete-default-completion-function) #'ignore) + (setq comint-input-autoexpand shell-input-autoexpand) + ;; Not needed in shell-mode because it's inherited from comint-mode, but + ;; placed here for read-shell-command. + (add-hook 'completion-at-point-functions 'comint-completion-at-point nil t)) + (put 'shell-mode 'mode-class 'special) (define-derived-mode shell-mode comint-mode "Shell" @@ -424,22 +469,12 @@ Variables `comint-output-filter-functions', a hook, and control whether input and output cause the window to scroll to the end of the buffer." (setq comint-prompt-regexp shell-prompt-pattern) - (setq comint-completion-fignore shell-completion-fignore) - (setq comint-delimiter-argument-list shell-delimiter-argument-list) - (setq comint-file-name-chars shell-file-name-chars) - (setq comint-file-name-quote-list shell-file-name-quote-list) - (set (make-local-variable 'comint-dynamic-complete-functions) - shell-dynamic-complete-functions) + (shell-completion-vars) (set (make-local-variable 'paragraph-separate) "\\'") - (make-local-variable 'paragraph-start) - (setq paragraph-start comint-prompt-regexp) - (make-local-variable 'font-lock-defaults) - (setq font-lock-defaults '(shell-font-lock-keywords t)) - (make-local-variable 'shell-dirstack) - (setq shell-dirstack nil) - (make-local-variable 'shell-last-dir) - (setq shell-last-dir nil) - (setq comint-input-autoexpand shell-input-autoexpand) + (set (make-local-variable 'paragraph-start) comint-prompt-regexp) + (set (make-local-variable 'font-lock-defaults) '(shell-font-lock-keywords t)) + (set (make-local-variable 'shell-dirstack) nil) + (set (make-local-variable 'shell-last-dir) nil) (shell-dirtrack-mode 1) ;; This is not really correct, since the shell buffer does not really ;; edit this directory. But it is useful in the buffer list and menus. @@ -447,7 +482,12 @@ buffer." ;; shell-dependent assignments. (when (ring-empty-p comint-input-ring) (let ((shell (file-name-nondirectory (car - (process-command (get-buffer-process (current-buffer))))))) + (process-command (get-buffer-process (current-buffer)))))) + (hsize (getenv "HISTSIZE"))) + (and (stringp hsize) + (integerp (setq hsize (string-to-number hsize))) + (> hsize 0) + (set (make-local-variable 'comint-input-ring-size) hsize)) (setq comint-input-ring-file-name (or (getenv "HISTFILE") (cond ((string-equal shell "bash") "~/.bash_history") @@ -469,8 +509,12 @@ buffer." (t "dirs"))) ;; Bypass a bug in certain versions of bash. (when (string-equal shell "bash") - (add-hook 'comint-output-filter-functions + (add-hook 'comint-preoutput-filter-functions 'shell-filter-ctrl-a-ctrl-b nil t))) + (when shell-dir-cookie-re + ;; Watch for magic cookies in the output to track the current dir. + (add-hook 'comint-output-filter-functions + 'shell-dir-cookie-watcher nil t)) (comint-read-input-ring t))) (defun shell-filter-ctrl-a-ctrl-b (string) @@ -483,15 +527,10 @@ started with the `--noediting' option and Select Graphic Rendition (SGR) control sequences (formerly known as ANSI escape sequences) are used to color the prompt. -This function can be put on `comint-output-filter-functions'. -The argument STRING is ignored." - (let ((pmark (process-mark (get-buffer-process (current-buffer))))) - (save-excursion - (goto-char (or (and (markerp comint-last-output-start) - (marker-position comint-last-output-start)) - (point-min))) - (while (re-search-forward "[\C-a\C-b]" pmark t) - (replace-match ""))))) +This function can be put on `comint-preoutput-filter-functions'." + (if (string-match "[\C-a\C-b]" string) + (replace-regexp-in-string "[\C-a\C-b]" "" string t t) + string)) (defun shell-write-history-on-exit (process event) "Called when the shell process is stopped. @@ -549,13 +588,34 @@ Otherwise, one argument `-i' is passed to the shell. (generate-new-buffer-name "*shell*")) (if (file-remote-p default-directory) ;; It must be possible to declare a local default-directory. + ;; FIXME: This can't be right: it changes the default-directory + ;; of the current-buffer rather than of the *shell* buffer. (setq default-directory (expand-file-name - (read-file-name + (read-directory-name "Default directory: " default-directory default-directory - t nil 'file-directory-p)))))))) + t nil)))))))) (require 'ansi-color) - (setq buffer (get-buffer-create (or buffer "*shell*"))) + (setq buffer (if (or buffer (not (derived-mode-p 'shell-mode)) + (comint-check-proc (current-buffer))) + (get-buffer-create (or buffer "*shell*")) + ;; If the current buffer is a dead shell buffer, use it. + (current-buffer))) + + ;; On remote hosts, the local `shell-file-name' might be useless. + (if (and (called-interactively-p 'any) + (file-remote-p default-directory) + (null explicit-shell-file-name) + (null (getenv "ESHELL"))) + (with-current-buffer buffer + (set (make-local-variable 'explicit-shell-file-name) + (file-remote-p + (expand-file-name + (read-file-name + "Remote shell path: " default-directory shell-file-name + t shell-file-name)) + 'localname)))) + ;; Pop to buffer, so that the buffer's window will be correctly set ;; when we call comint (so that comint sets the COLUMNS env var properly). (pop-to-buffer buffer) @@ -618,6 +678,20 @@ Otherwise, one argument `-i' is passed to the shell. ;; replace it with a process filter that watches for and strips out ;; these messages. +(defun shell-dir-cookie-watcher (text) + ;; This is fragile: the TEXT could be split into several chunks and we'd + ;; miss it. Oh well. It's a best effort anyway. I'd expect that it's + ;; rather unusual to have the prompt split into several packets, but + ;; I'm sure Murphy will prove me wrong. + (when (and shell-dir-cookie-re (string-match shell-dir-cookie-re text)) + (let ((dir (match-string 1 text))) + (cond + ((file-name-absolute-p dir) (shell-cd dir)) + ;; Let's try and see if it seems to be up or down from where we were. + ((string-match "\\`\\(.*\\)\\(?:/.*\\)?\n\\(.*/\\)\\1\\(?:/.*\\)?\\'" + (setq text (concat dir "\n" default-directory))) + (shell-cd (concat (match-string 2 text) dir))))))) + (defun shell-directory-tracker (str) "Tracks cd, pushd and popd commands issued to the shell. This function is called on each input passed to the shell. @@ -636,11 +710,12 @@ and `shell-pushd-dunique' control the behavior of the relevant command. Environment variables are expanded, see function `substitute-in-file-name'." (if shell-dirtrackp ;; We fail gracefully if we think the command will fail in the shell. - (condition-case chdir-failure + (condition-case nil (let ((start (progn (string-match (concat "^" shell-command-separator-regexp) str) ; skip whitespace (match-end 0))) + (case-fold-search) end cmd arg1) (while (string-match shell-command-regexp str start) (setq end (match-end 0) @@ -928,7 +1003,7 @@ Copy Shell environment variable to Emacs: "))) "Move forward across ARG shell command(s). Does not cross lines. See `shell-command-regexp'." (interactive "p") - (let ((limit (save-excursion (end-of-line nil) (point)))) + (let ((limit (line-end-position))) (if (re-search-forward (concat shell-command-regexp "\\([;&|][\t ]*\\)+") limit 'move arg) (skip-syntax-backward " ")))) @@ -955,30 +1030,36 @@ candidates. Note that this may not be the same as the shell's idea of the path. Completion is dependent on the value of `shell-completion-execonly', plus -those that effect file completion. See `shell-dynamic-complete-as-command'. +those that effect file completion. Returns t if successful." (interactive) + (let ((data (shell-command-completion))) + (if data + (prog2 (unless (window-minibuffer-p (selected-window)) + (message "Completing command name...")) + (apply #'completion-in-region data))))) + +(defun shell-command-completion () + "Return the completion data for the command at point, if any." (let ((filename (comint-match-partial-filename))) (if (and filename (save-match-data (not (string-match "[~/]" filename))) (eq (match-beginning 0) (save-excursion (shell-backward-command 1) (point)))) - (prog2 (unless (window-minibuffer-p (selected-window)) - (message "Completing command name...")) - (shell-dynamic-complete-as-command))))) - + (shell--command-completion-data)))) -(defun shell-dynamic-complete-as-command () - "Dynamically complete at point as a command. -See `shell-dynamic-complete-filename'. Returns t if successful." +(defun shell--command-completion-data () + "Return the completion data for the command at point." (let* ((filename (or (comint-match-partial-filename) "")) + (start (if (zerop (length filename)) (point) (match-beginning 0))) + (end (if (zerop (length filename)) (point) (match-end 0))) (filenondir (file-name-nondirectory filename)) - (path-dirs (cdr (reverse exec-path))) + (path-dirs (cdr (reverse exec-path))) ;FIXME: Why `cdr'? (cwd (file-name-as-directory (expand-file-name default-directory))) (ignored-extensions (and comint-completion-fignore - (mapconcat (function (lambda (x) (concat (regexp-quote x) "$"))) + (mapconcat (function (lambda (x) (concat (regexp-quote x) "\\'"))) comint-completion-fignore "\\|"))) (dir "") (comps-in-dir ()) (file "") (abs-file-name "") (completions ())) @@ -1002,18 +1083,34 @@ See `shell-dynamic-complete-filename'. Returns t if successful." (setq comps-in-dir (cdr comps-in-dir))) (setq path-dirs (cdr path-dirs))) ;; OK, we've got a list of completions. - (let ((success (let ((comint-completion-addsuffix nil)) - (comint-dynamic-simple-complete filenondir completions)))) - (if (and (memq success '(sole shortest)) comint-completion-addsuffix - (not (file-directory-p (comint-match-partial-filename)))) - (insert " ")) - success))) + (list + start end + (lambda (string pred action) + (if (string-match "/" string) + (completion-file-name-table string pred action) + (complete-with-action action completions string pred))) + :exit-function + (lambda (_string finished) + (when (memq finished '(sole finished)) + (if (looking-at " ") + (goto-char (match-end 0)) + (insert " "))))))) + +;; (defun shell-dynamic-complete-as-command () +;; "Dynamically complete at point as a command. +;; See `shell-dynamic-complete-filename'. Returns t if successful." +;; (apply #'completion-in-region shell--command-completion-data)) (defun shell-dynamic-complete-filename () "Dynamically complete the filename at point. This completes only if point is at a suitable position for a filename argument." (interactive) + (let ((data (shell-filename-completion))) + (if data (apply #'completion-in-region data)))) + +(defun shell-filename-completion () + "Return the completion data for file name at point, if any." (let ((opoint (point)) (beg (comint-line-beginning-position))) (when (save-excursion @@ -1021,24 +1118,21 @@ filename argument." (match-end 0) beg)) (re-search-forward "[^ \t][ \t]" opoint t)) - (comint-dynamic-complete-as-filename)))) + (comint-filename-completion)))) (defun shell-match-partial-variable () "Return the shell variable at point, or nil if none is found." (save-excursion - (let ((limit (point))) - (if (re-search-backward "[^A-Za-z0-9_{}]" nil 'move) - (or (looking-at "\\$") (forward-char 1))) - ;; Anchor the search forwards. - (if (or (eolp) (looking-at "[^A-Za-z0-9_{}$]")) - nil - (re-search-forward "\\$?{?[A-Za-z0-9_]*}?" limit) - (buffer-substring (match-beginning 0) (match-end 0)))))) + (if (re-search-backward "[^A-Za-z0-9_{(]" nil 'move) + (or (looking-at "\\$") (forward-char 1))) + (if (or (eolp) (looking-at "[^A-Za-z0-9_{($]")) + nil + (looking-at "\\$?[{(]?[A-Za-z0-9_]*[})]?") + (buffer-substring (match-beginning 0) (match-end 0))))) (defun shell-dynamic-complete-environment-variable () "Dynamically complete the environment variable at point. Completes if after a variable, i.e., if it starts with a \"$\". -See `shell-dynamic-complete-as-environment-variable'. This function is similar to `comint-dynamic-complete-filename', except that it searches `process-environment' for completion candidates. Note that this may @@ -1050,38 +1144,68 @@ called `shell-dynamic-complete-process-environment-variable'. Returns non-nil if successful." (interactive) - (let ((variable (shell-match-partial-variable))) - (if (and variable (string-match "^\\$" variable)) + (let ((data (shell-environment-variable-completion))) + (if data (prog2 (unless (window-minibuffer-p (selected-window)) (message "Completing variable name...")) - (shell-dynamic-complete-as-environment-variable))))) - - -(defun shell-dynamic-complete-as-environment-variable () - "Dynamically complete at point as an environment variable. -Used by `shell-dynamic-complete-environment-variable'. -Uses `comint-dynamic-simple-complete'." - (let* ((var (or (shell-match-partial-variable) "")) - (variable (substring var (or (string-match "[^$({]\\|$" var) 0))) - (variables (mapcar (function (lambda (x) - (substring x 0 (string-match "=" x)))) - process-environment)) - (addsuffix comint-completion-addsuffix) - (comint-completion-addsuffix nil) - (success (comint-dynamic-simple-complete variable variables))) - (if (memq success '(sole shortest)) - (let* ((var (shell-match-partial-variable)) - (variable (substring var (string-match "[^$({]" var))) - (protection (cond ((string-match "{" var) "}") - ((string-match "(" var) ")") - (t ""))) - (suffix (cond ((null addsuffix) "") - ((file-directory-p - (comint-directory (getenv variable))) "/") - (t " ")))) - (insert protection suffix))) - success)) - + (apply #'completion-in-region data))))) + + +(defun shell-environment-variable-completion () + "Completion data for an environment variable at point, if any." + (let* ((var (shell-match-partial-variable)) + (end (match-end 0))) + (when (and (not (zerop (length var))) (eq (aref var 0) ?$)) + (let* ((start + (save-excursion + (goto-char (match-beginning 0)) + (looking-at "\\$?[({]*") + (match-end 0))) + (variables (mapcar (lambda (x) + (substring x 0 (string-match "=" x))) + process-environment)) + (suffix (case (char-before start) (?\{ "}") (?\( ")") (t "")))) + (list start end variables + :exit-function + (lambda (s finished) + (when (memq finished '(sole finished)) + (let ((suf (concat suffix + (if (file-directory-p + (comint-directory (getenv s))) + "/")))) + (if (looking-at (regexp-quote suf)) + (goto-char (match-end 0)) + (insert suf)))))))))) + + +(defun shell-c-a-p-replace-by-expanded-directory () + "Expand directory stack reference before point. +For use on `completion-at-point-functions'." + (when (comint-match-partial-filename) + (save-excursion + (goto-char (match-beginning 0)) + (let ((stack (cons default-directory shell-dirstack)) + (index (cond ((looking-at "=-/?") + (length shell-dirstack)) + ((looking-at "=\\([0-9]+\\)/?") + (string-to-number + (buffer-substring + (match-beginning 1) (match-end 1))))))) + (when index + (let ((start (match-beginning 0)) + (end (match-end 0)) + (replacement (file-name-as-directory (nth index stack)))) + (lambda () + (cond + ((>= index (length stack)) + (error "Directory stack not that deep")) + (t + (save-excursion + (goto-char start) + (insert replacement) + (delete-char (- end start))) + (message "Directory item: %d" index) + t))))))))) (defun shell-replace-by-expanded-directory () "Expand directory stack reference before point. @@ -1090,26 +1214,9 @@ See `default-directory' and `shell-dirstack'. Returns t if successful." (interactive) - (if (comint-match-partial-filename) - (save-excursion - (goto-char (match-beginning 0)) - (let ((stack (cons default-directory shell-dirstack)) - (index (cond ((looking-at "=-/?") - (length shell-dirstack)) - ((looking-at "=\\([0-9]+\\)/?") - (string-to-number - (buffer-substring - (match-beginning 1) (match-end 1))))))) - (cond ((null index) - nil) - ((>= index (length stack)) - (error "Directory stack not that deep")) - (t - (replace-match (file-name-as-directory (nth index stack)) t t) - (message "Directory item: %d" index) - t)))))) + (let ((f (shell-c-a-p-replace-by-expanded-directory))) + (if f (funcall f)))) (provide 'shell) -;; arch-tag: bcb5f12a-c1f4-4aea-a809-2504bd5bd797 ;;; shell.el ends here