;;; sh-script.el --- shell-script editing commands for Emacs
-;; Copyright (C) 1993-1997, 1999, 2001-2011 Free Software Foundation, Inc.
+;; Copyright (C) 1993-1997, 1999, 2001-2013 Free Software Foundation,
+;; Inc.
;; Author: Daniel Pfeiffer <occitan@esperanto.org>
;; Version: 2.0f
(eval-when-compile
(require 'skeleton)
- (require 'cl)
+ (require 'cl-lib)
(require 'comint))
(require 'executable)
+(autoload 'comint-completion-at-point "comint")
+(autoload 'comint-filename-completion "comint")
+(autoload 'shell-command-completion "shell")
+(autoload 'shell-environment-variable-completion "shell")
+
(defvar font-lock-comment-face)
(defvar font-lock-set-defaults)
(defvar font-lock-string-face)
(defcustom sh-imenu-generic-expression
`((sh
- . ((nil "^\\s-*\\(function\\s-+\\)?\\([[:alpha:]_][[:alnum:]_]+\\)\\s-*()" 2))))
+ . ((nil
+ ;; function FOO
+ ;; function FOO()
+ "^\\s-*function\\s-+\\\([[:alpha:]_][[:alnum:]_]+\\)\\s-*\\(?:()\\)?"
+ 1)
+ ;; FOO()
+ (nil
+ "^\\s-*\\([[:alpha:]_][[:alnum:]_]+\\)\\s-*()"
+ 1)
+ )))
"Alist of regular expressions for recognizing shell function definitions.
See `sh-feature' and `imenu-generic-expression'."
:type '(alist :key-type (symbol :tag "Shell")
(define-key map "\C-c+" 'sh-add)
(define-key map "\C-\M-x" 'sh-execute-region)
(define-key map "\C-c\C-x" 'executable-interpret)
- ;; FIXME: Use post-self-insert-hook.
- (define-key map "<" 'sh-maybe-here-document)
- (define-key map "(" 'skeleton-pair-insert-maybe)
- (define-key map "{" 'skeleton-pair-insert-maybe)
- (define-key map "[" 'skeleton-pair-insert-maybe)
- (define-key map "'" 'skeleton-pair-insert-maybe)
- (define-key map "`" 'skeleton-pair-insert-maybe)
- (define-key map "\"" 'skeleton-pair-insert-maybe)
-
- (define-key map [remap complete-tag] 'comint-dynamic-complete)
+
(define-key map [remap delete-backward-char]
'backward-delete-char-untabify)
(define-key map "\C-c:" 'sh-set-shell)
(define-key map [menu-bar sh-script] (cons "Sh-Script" menu-map))
(define-key menu-map [sh-learn-buffer-indent]
'(menu-item "Learn buffer indentation" sh-learn-buffer-indent
- :help "Learn how to indent the buffer the way it currently is."))
+ :help "Learn how to indent the buffer the way it currently is."))
(define-key menu-map [sh-learn-line-indent]
'(menu-item "Learn line indentation" sh-learn-line-indent
- :help "Learn how to indent a line as it currently is indented"))
+ :help "Learn how to indent a line as it currently is indented"))
(define-key menu-map [sh-show-indent]
'(menu-item "Show indentation" sh-show-indent
:help "Show the how the current line would be indented"))
(define-key menu-map [sh-pair]
'(menu-item "Insert braces and quotes in pairs"
- (lambda ()
- (interactive)
- (require 'skeleton)
- (setq skeleton-pair (not skeleton-pair)))
- :button (:toggle . (and (boundp 'skeleton-pair)
- skeleton-pair))
- :help "Inserting a brace or quote automatically inserts the matching pair"))
+ electric-pair-mode
+ :button (:toggle . (bound-and-true-p electric-pair-mode))
+ :help "Inserting a brace or quote automatically inserts the matching pair"))
(define-key menu-map [sh-s0] '("--"))
;; Insert
:help "Insert a function definition"))
(define-key menu-map [sh-add]
'(menu-item "Addition..." sh-add
- :help "Insert an addition of VAR and prefix DELTA for Bourne (type) shell"))
+ :help "Insert an addition of VAR and prefix DELTA for Bourne (type) shell"))
(define-key menu-map [sh-until]
'(menu-item "Until Loop" sh-until
:help "Insert an until loop"))
(define-key menu-map [sh-s1] '("--"))
(define-key menu-map [sh-exec]
'(menu-item "Execute region" sh-execute-region
- :help "Pass optional header and region to a subshell for noninteractive execution"))
+ :help "Pass optional header and region to a subshell for noninteractive execution"))
(define-key menu-map [sh-exec-interpret]
'(menu-item "Execute script..." executable-interpret
- :help "Run script with user-specified args, and collect output in a buffer"))
+ :help "Run script with user-specified args, and collect output in a buffer"))
(define-key menu-map [sh-set-shell]
'(menu-item "Set shell type..." sh-set-shell
:help "Set this buffer's shell to SHELL (a string)"))
(define-key menu-map [sh-backslash-region]
'(menu-item "Backslash region" sh-backslash-region
- :help "Insert, align, or delete end-of-line backslashes on the lines in the region."))
+ :help "Insert, align, or delete end-of-line backslashes on the lines in the region."))
map)
"Keymap used in Shell-Script mode.")
"Value to use for `skeleton-pair-default-alist' in Shell-Script mode.")
(defcustom sh-dynamic-complete-functions
- '(shell-dynamic-complete-environment-variable
- shell-dynamic-complete-command
- comint-dynamic-complete-filename)
+ '(shell-environment-variable-completion
+ shell-command-completion
+ comint-filename-completion)
"Functions for doing TAB dynamic completion."
:type '(repeat function)
:group 'sh-script)
(defcustom sh-assignment-regexp
- '((csh . "\\<\\([[:alnum:]_]+\\)\\(\\[.+\\]\\)?[ \t]*[-+*/%^]?=")
+ `((csh . "\\<\\([[:alnum:]_]+\\)\\(\\[.+\\]\\)?[ \t]*[-+*/%^]?=")
;; actually spaces are only supported in let/(( ... ))
- (ksh88 . "\\<\\([[:alnum:]_]+\\)\\(\\[.+\\]\\)?[ \t]*\\([-+*/%&|~^]\\|<<\\|>>\\)?=")
+ (ksh88 . ,(concat "\\<\\([[:alnum:]_]+\\)\\(\\[.+\\]\\)?"
+ "[ \t]*\\(?:[-+*/%&|~^]\\|<<\\|>>\\)?="))
+ (bash . "\\<\\([[:alnum:]_]+\\)\\(\\[.+\\]\\)?\\+?=")
(rc . "\\<\\([[:alnum:]_*]+\\)[ \t]*=")
(sh . "\\<\\([[:alnum:]_]+\\)="))
"Regexp for the variable name and what may follow in an assignment.
font-lock-variable-name-face))
(rc sh-append es)
- (bash sh-append shell ("\\$(\\(\\sw+\\)" (1 'sh-quoted-exec t) ))
+ (bash sh-append sh ("\\$(\\(\\sw+\\)" (1 'sh-quoted-exec t) ))
(sh sh-append shell
;; Variable names.
("\\$\\({#?\\)?\\([[:alpha:]_][[:alnum:]_]*\\|[-#?@!]\\)" 2
(concat "<<-?\\s-*\\\\?\\(\\(?:['\"][^'\"]+['\"]\\|\\sw\\|[-/~._]\\)+\\)"
sh-escaped-line-re "\\(\n\\)")))
+(defun sh--inside-noncommand-expression (pos)
+ (save-excursion
+ (let ((ppss (syntax-ppss pos)))
+ (when (nth 1 ppss)
+ (goto-char (nth 1 ppss))
+ (pcase (char-after)
+ ;; $((...)) or $[...] or ${...}.
+ (`?\( (and (eq ?\( (char-before))
+ (eq ?\$ (char-before (1- (point))))))
+ ((or `?\{ `?\[) (eq ?\$ (char-before))))))))
+
(defun sh-font-lock-open-heredoc (start string eol)
"Determine the syntax of the \\n after a <<EOF.
START is the position of <<.
be indented (i.e. a <<- was used rather than just <<).
Point is at the beginning of the next line."
(unless (or (memq (char-before start) '(?< ?>))
- (sh-in-comment-or-string start))
+ (sh-in-comment-or-string start)
+ (sh--inside-noncommand-expression start))
;; We're looking at <<STRING, so we add "^STRING$" to the syntactic
;; font-lock keywords to detect the end of this here document.
(let ((str (replace-regexp-in-string "['\"]" "" string))
subshells can nest."
;; FIXME: This can (and often does) match multiple lines, yet it makes no
;; effort to handle multiline cases correctly, so it ends up being
- ;; rather flakey.
+ ;; rather flaky.
(when (eq ?\" (nth 3 (syntax-ppss))) ; Check we matched an opening quote.
;; bingo we have a $( or a ` inside a ""
(let (;; `state' can be: double-quote, backquote, code.
(while (and state (progn (skip-chars-forward "^'\\\\\"`$()" limit)
(< (point) limit)))
;; unescape " inside a $( ... ) construct.
- (case (char-after)
- (?\' (case state
- (double-quote nil)
- (t (forward-char 1) (skip-chars-forward "^'" limit))))
+ (pcase (char-after)
+ (?\' (pcase state
+ (`double-quote nil)
+ (_ (forward-char 1) (skip-chars-forward "^'" limit))))
(?\\ (forward-char 1))
- (?\" (case state
- (double-quote (setq state (pop states)))
- (t (push state states) (setq state 'double-quote)))
+ (?\" (pcase state
+ (`double-quote (setq state (pop states)))
+ (_ (push state states) (setq state 'double-quote)))
(if state (put-text-property (point) (1+ (point))
'syntax-table '(1))))
- (?\` (case state
- (backquote (setq state (pop states)))
- (t (push state states) (setq state 'backquote))))
+ (?\` (pcase state
+ (`backquote (setq state (pop states)))
+ (_ (push state states) (setq state 'backquote))))
(?\$ (if (not (eq (char-after (1+ (point))) ?\())
nil
(forward-char 1)
- (case state
- (t (push state states) (setq state 'code)))))
- (?\( (case state
- (double-quote nil)
- (t (push state states) (setq state 'code))))
- (?\) (case state
- (double-quote nil)
- (t (setq state (pop states)))))
- (t (error "Internal error in sh-font-lock-quoted-subshell")))
+ (pcase state
+ (_ (push state states) (setq state 'code)))))
+ (?\( (pcase state
+ (`double-quote nil)
+ (_ (push state states) (setq state 'code))))
+ (?\) (pcase state
+ (`double-quote nil)
+ (_ (setq state (pop states)))))
+ (_ (error "Internal error in sh-font-lock-quoted-subshell")))
(forward-char 1)))))
(defun sh-font-lock-paren (start)
(unless (nth 8 (syntax-ppss))
(save-excursion
- (goto-char start)
- ;; Skip through all patterns
- (while
- (progn
- (while
- (progn
- (forward-comment (- (point-max)))
- (when (and (eolp) (sh-is-quoted-p (point)))
- (forward-char -1)
- t)))
- ;; Skip through one pattern
- (while
- (or (/= 0 (skip-syntax-backward "w_"))
- (/= 0 (skip-chars-backward "-$=?[]*@/\\\\"))
- (and (sh-is-quoted-p (1- (point)))
- (goto-char (- (point) 2)))
- (when (memq (char-before) '(?\" ?\' ?\}))
- (condition-case nil (progn (backward-sexp 1) t)
- (error nil)))))
- ;; Patterns can be preceded by an open-paren (Bug#1320).
- (if (eq (char-before (point)) ?\()
+ (let ((open nil))
+ (goto-char start)
+ ;; Skip through all patterns
+ (while
+ (progn
+ (while
+ (progn
+ (forward-comment (- (point-max)))
+ (when (and (eolp) (sh-is-quoted-p (point)))
+ (forward-char -1)
+ t)))
+ ;; Skip through one pattern
+ (while
+ (or (/= 0 (skip-syntax-backward "w_"))
+ (/= 0 (skip-chars-backward "-$=?[]*@/\\\\"))
+ (and (sh-is-quoted-p (1- (point)))
+ (goto-char (- (point) 2)))
+ (when (memq (char-before) '(?\" ?\' ?\}))
+ (condition-case nil (progn (backward-sexp 1) t)
+ (error nil)))))
+ ;; Patterns can be preceded by an open-paren (bug#1320).
+ (when (eq (char-before (point)) ?\()
+ (backward-char 1)
+ (setq open (point)))
+ (while (progn
+ (forward-comment (- (point-max)))
+ ;; Maybe we've bumped into an escaped newline.
+ (sh-is-quoted-p (point)))
(backward-char 1))
- (while (progn
- (forward-comment (- (point-max)))
- ;; Maybe we've bumped into an escaped newline.
- (sh-is-quoted-p (point)))
- (backward-char 1))
- (when (eq (char-before) ?|)
- (backward-char 1) t)))
- (when (progn (backward-char 2)
- (if (> start (line-end-position))
- (put-text-property (point) (1+ start)
- 'syntax-multiline t))
- ;; FIXME: The `in' may just be a random argument to
- ;; a normal command rather than the real `in' keyword.
- ;; I.e. we should look back to try and find the
- ;; corresponding `case'.
- (and (looking-at ";[;&]\\|\\_<in")
- ;; ";; esac )" is a case that looks like a case-pattern
- ;; but it's really just a close paren after a case
- ;; statement. I.e. if we skipped over `esac' just now,
- ;; we're not looking at a case-pattern.
- (not (looking-at "..[ \t\n]+esac[^[:word:]_]"))))
- sh-st-punc))))
+ (when (eq (char-before) ?|)
+ (backward-char 1) t)))
+ (and (> (point) (1+ (point-min)))
+ (progn (backward-char 2)
+ (if (> start (line-end-position))
+ (put-text-property (point) (1+ start)
+ 'syntax-multiline t))
+ ;; FIXME: The `in' may just be a random argument to
+ ;; a normal command rather than the real `in' keyword.
+ ;; I.e. we should look back to try and find the
+ ;; corresponding `case'.
+ (and (looking-at ";[;&]\\|\\_<in")
+ ;; ";; esac )" is a case that looks
+ ;; like a case-pattern but it's really just a close
+ ;; paren after a case statement. I.e. if we skipped
+ ;; over `esac' just now, we're not looking
+ ;; at a case-pattern.
+ (not (looking-at "..[ \t\n]+esac[^[:word:]_]"))))
+ (progn
+ (when open
+ (put-text-property open (1+ open) 'syntax-table sh-st-punc))
+ sh-st-punc))))))
(defun sh-font-lock-backslash-quote ()
(if (eq (save-excursion (nth 3 (syntax-ppss (match-beginning 0)))) ?\')
;; metacharacters. The list of special chars is taken from
;; the single-unix spec of the shell command language (under
;; `quoting') but with `$' removed.
- ("[^|&;<>()`\\\"' \t\n]\\(#+\\)" (1 "_"))
+ ("\\(?:[^|&;<>()`\\\"' \t\n]\\|\\${\\)\\(#+\\)" (1 "_"))
;; In a '...' the backslash is not escaping.
("\\(\\\\\\)'" (1 (sh-font-lock-backslash-quote)))
;; Make sure $@ and $? are correctly recognized as sexps.
(")" (0 (sh-font-lock-paren (match-beginning 0))))
;; Highlight (possibly nested) subshells inside "" quoted
;; regions correctly.
- ("\"\\(?:\\(?:[^\\\"]\\|\\)*?[^\\]\\(?:\\\\\\\\\\)*\\)??\\(\\$(\\|`\\)"
+ ("\"\\(?:\\(?:[^\\\"]\\|\\\\.\\)*?\\)??\\(\\$(\\|`\\)"
(1 (ignore
- ;; Save excursion because we want to also apply other
- ;; syntax-propertize rules within the affected region.
- (if (nth 8 (syntax-ppss))
+ (if (nth 8 (save-excursion (syntax-ppss (match-beginning 0))))
(goto-char (1+ (match-beginning 0)))
+ ;; Save excursion because we want to also apply other
+ ;; syntax-propertize rules within the affected region.
(save-excursion
(sh-font-lock-quoted-subshell end)))))))
(point) end))
-
(defun sh-font-lock-syntactic-face-function (state)
(let ((q (nth 3 state)))
(if q
:group 'sh-indentation)
(put 'sh-basic-offset 'safe-local-variable 'integerp)
-(defcustom sh-indent-comment nil
+(defcustom sh-indent-comment t
"How a comment line is to be indented.
nil means leave it as it is;
t means indent it as a normal line, aligning it to previous non-blank
(const :tag "Indent as a normal line." t)
(integer :menu-tag "Indent to this col (0 means first col)."
:tag "Indent to column number.") )
+ :version "24.3"
:group 'sh-indentation)
(defconst sh-indent-supported
- '((sh . t)
+ '((sh . sh)
(csh . nil)
- (rc . t))
- "Shell types that shell indenting can do something with.")
+ (rc . rc))
+ "Indentation rule set to use for each shell type.")
(defvar sh-indent-supported-here nil
"Non-nil if we support indentation for the current buffer's shell type.")
values of variables, and for the commands using indentation styles.")
(defvar sh-make-vars-local t
- "*Controls whether indentation variables are local to the buffer.
+ "Controls whether indentation variables are local to the buffer.
If non-nil, indentation variables are made local initially.
If nil, you can later make the variables local by invoking
command `sh-make-vars-local'.
\\[sh-set-shell] Set this buffer's shell, and maybe its magic number.
\\[sh-execute-region] Have optional header and region be executed in a subshell.
-\\[sh-maybe-here-document] Without prefix, following an unquoted < inserts here document.
-\{, (, [, ', \", `
- Unless quoted with \\, insert the pairs {}, (), [], or '', \"\", ``.
+`sh-electric-here-document-mode' controls whether insertion of two
+unquoted < insert a here document.
If you generally program a shell different from your login shell you can
set `sh-shell-file' accordingly. If your shell's file name doesn't correctly
(make-local-variable 'sh-shell-file)
(make-local-variable 'sh-shell)
- (set (make-local-variable 'skeleton-pair-default-alist)
- sh-skeleton-pair-default-alist)
- (set (make-local-variable 'skeleton-end-hook)
- (lambda () (or (eolp) (newline) (indent-relative))))
-
- (set (make-local-variable 'paragraph-start) (concat page-delimiter "\\|$"))
- (set (make-local-variable 'paragraph-separate) paragraph-start)
- (set (make-local-variable 'comment-start) "# ")
- (set (make-local-variable 'comment-start-skip) "#+[\t ]*")
- (set (make-local-variable 'local-abbrev-table) sh-mode-abbrev-table)
- (set (make-local-variable 'comint-dynamic-complete-functions)
- sh-dynamic-complete-functions)
+ (setq-local skeleton-pair-default-alist
+ sh-skeleton-pair-default-alist)
+ (setq-local skeleton-end-hook
+ (lambda () (or (eolp) (newline) (indent-relative))))
+
+ (setq-local paragraph-start (concat page-delimiter "\\|$"))
+ (setq-local paragraph-separate paragraph-start)
+ (setq-local comment-start "# ")
+ (setq-local comment-start-skip "#+[\t ]*")
+ (setq-local local-abbrev-table sh-mode-abbrev-table)
+ (setq-local comint-dynamic-complete-functions
+ sh-dynamic-complete-functions)
+ (add-hook 'completion-at-point-functions 'comint-completion-at-point nil t)
;; we can't look if previous line ended with `\'
- (set (make-local-variable 'comint-prompt-regexp) "^[ \t]*")
- (set (make-local-variable 'imenu-case-fold-search) nil)
- (set (make-local-variable 'font-lock-defaults)
- `((sh-font-lock-keywords
- sh-font-lock-keywords-1 sh-font-lock-keywords-2)
- nil nil
- ((?/ . "w") (?~ . "w") (?. . "w") (?- . "w") (?_ . "w")) nil
- (font-lock-syntactic-face-function
- . sh-font-lock-syntactic-face-function)))
- (set (make-local-variable 'syntax-propertize-function)
- #'sh-syntax-propertize-function)
+ (setq-local comint-prompt-regexp "^[ \t]*")
+ (setq-local imenu-case-fold-search nil)
+ (setq font-lock-defaults
+ `((sh-font-lock-keywords
+ sh-font-lock-keywords-1 sh-font-lock-keywords-2)
+ nil nil
+ ((?/ . "w") (?~ . "w") (?. . "w") (?- . "w") (?_ . "w")) nil
+ (font-lock-syntactic-face-function
+ . sh-font-lock-syntactic-face-function)))
+ (setq-local syntax-propertize-function #'sh-syntax-propertize-function)
(add-hook 'syntax-propertize-extend-region-functions
#'syntax-propertize-multiline 'append 'local)
- (set (make-local-variable 'skeleton-pair-alist) '((?` _ ?`)))
- (set (make-local-variable 'skeleton-pair-filter-function) 'sh-quoted-p)
- (set (make-local-variable 'skeleton-further-elements)
- '((< '(- (min sh-indentation (current-column))))))
- (set (make-local-variable 'skeleton-filter-function) 'sh-feature)
- (set (make-local-variable 'skeleton-newline-indent-rigidly) t)
- (set (make-local-variable 'sh-indent-supported-here) nil)
- (set (make-local-variable 'defun-prompt-regexp)
- (concat "^\\(function[ \t]\\|[[:alnum:]]+[ \t]+()[ \t]+\\)"))
+ (sh-electric-here-document-mode 1)
+ (setq-local skeleton-pair-alist '((?` _ ?`)))
+ (setq-local skeleton-pair-filter-function 'sh-quoted-p)
+ (setq-local skeleton-further-elements
+ '((< '(- (min sh-indentation (current-column))))))
+ (setq-local skeleton-filter-function 'sh-feature)
+ (setq-local skeleton-newline-indent-rigidly t)
+ (setq-local defun-prompt-regexp
+ (concat "^\\(function[ \t]\\|[[:alnum:]]+[ \t]+()[ \t]+\\)"))
;; Parse or insert magic number for exec, and set all variables depending
;; on the shell thus determined.
(sh-set-shell
(goto-char (point-min))
(looking-at "#![ \t]?\\([^ \t\n]*/bin/env[ \t]\\)?\\([^ \t\n]+\\)"))
(match-string 2))
- ((not buffer-file-name)
- sh-shell-file)
+ ((not buffer-file-name) sh-shell-file)
;; Checks that use `buffer-file-name' follow.
- ((string-match "\\.m?spec\\'" buffer-file-name)
- "rpm")
- ((string-match "[.]sh\\>" buffer-file-name)
- "sh")
- ((string-match "[.]bash\\>" buffer-file-name)
- "bash")
- ((string-match "[.]ksh\\>" buffer-file-name)
- "ksh")
- ((string-match "[.]csh\\>" buffer-file-name)
- "csh")
- ((equal (file-name-nondirectory buffer-file-name) ".profile")
- "sh")
- (t
- sh-shell-file))
+ ((string-match "\\.m?spec\\'" buffer-file-name) "rpm")
+ ((string-match "[.]sh\\>" buffer-file-name) "sh")
+ ((string-match "[.]bash\\>" buffer-file-name) "bash")
+ ((string-match "[.]ksh\\>" buffer-file-name) "ksh")
+ ((string-match "[.]csh\\>" buffer-file-name) "csh")
+ ((equal (file-name-nondirectory buffer-file-name) ".profile") "sh")
+ (t sh-shell-file))
nil nil))
;;;###autoload
"Function to get better fontification including keywords and builtins."
(sh-font-lock-keywords-1 t))
+;;; Indentation and navigation with SMIE.
+
+(require 'smie)
+
+;; The SMIE code should generally be preferred, but it currently does not obey
+;; the various indentation custom-vars, and it misses some important features
+;; of the old code, mostly: sh-learn-line/buffer-indent, sh-show-indent,
+;; sh-name/save/load-style.
+(defvar sh-use-smie nil
+ "Whether to use the SMIE code for navigation and indentation.")
+
+(defun sh-smie--keyword-p (tok)
+ "Non-nil if TOK (at which we're looking) really is a keyword."
+ (let ((prev (funcall smie-backward-token-function)))
+ (if (zerop (length prev))
+ (looking-back "\\s(" (1- (point)))
+ (assoc prev smie-grammar))))
+
+(defun sh-smie--newline-semi-p (&optional tok)
+ "Return non-nil if a newline should be treated as a semi-colon.
+Here we assume that a newline should be treated as a semi-colon unless it
+comes right after a special keyword.
+This function does not pay attention to line-continuations.
+If TOK is nil, point should be before the newline; otherwise, TOK is the token
+before the newline and in that case point should be just before the token."
+ (save-excursion
+ (unless tok
+ (setq tok (funcall smie-backward-token-function)))
+ (if (and (zerop (length tok))
+ (looking-back "\\s(" (1- (point))))
+ nil
+ (not (numberp (nth 2 (assoc tok smie-grammar)))))))
+
+;;;; SMIE support for `sh'.
+
+(defconst sh-smie-sh-grammar
+ (smie-prec2->grammar
+ (smie-bnf->prec2
+ '((exp) ;A constant, or a $var, or a sequence of them...
+ (cmd ("case" exp "in" branches "esac")
+ ("if" cmd "then" cmd "fi")
+ ("if" cmd "then" cmd "else" cmd "fi")
+ ("if" cmd "then" cmd "elif" cmd "then" cmd "fi")
+ ("if" cmd "then" cmd "elif" cmd "then" cmd "else" cmd "fi")
+ ("if" cmd "then" cmd "elif" cmd "then" cmd
+ "elif" cmd "then" cmd "else" cmd "fi")
+ ("while" cmd "do" cmd "done")
+ ("until" cmd "do" cmd "done")
+ ("for" exp "in" cmd "do" cmd "done")
+ ("for" exp "do" cmd "done")
+ ("select" exp "in" cmd "do" cmd "done") ;bash&zsh&ksh88.
+ ("repeat" exp "do" cmd "done") ;zsh.
+ (exp "always" exp) ;zsh.
+ (cmd "|" cmd) (cmd "|&" cmd)
+ (cmd "&&" cmd) (cmd "||" cmd)
+ (cmd ";" cmd) (cmd "&" cmd))
+ (rpattern (rpattern "|" rpattern))
+ (pattern (rpattern) ("case-(" rpattern))
+ (branches (branches ";;" branches)
+ (branches ";&" branches) (branches ";;&" branches) ;bash.
+ (pattern "case-)" cmd)))
+ '((assoc ";;" ";&" ";;&"))
+ '((assoc ";" "&") (assoc "&&" "||") (assoc "|" "|&")))))
+
+(defconst sh-smie--sh-operators
+ (delq nil (mapcar (lambda (x)
+ (setq x (car x))
+ (and (stringp x)
+ (not (string-match "\\`[a-z]" x))
+ x))
+ sh-smie-sh-grammar)))
+
+(defconst sh-smie--sh-operators-re (regexp-opt sh-smie--sh-operators))
+(defconst sh-smie--sh-operators-back-re
+ (concat "\\(?:^\\|[^\\]\\)\\(?:\\\\\\\\\\)*"
+ "\\(" sh-smie--sh-operators-re "\\)"))
+
+(defun sh-smie--sh-keyword-in-p ()
+ "Assuming we're looking at \"in\", return non-nil if it's a keyword.
+Does not preserve point."
+ (let ((forward-sexp-function nil)
+ (words nil) ;We've seen words.
+ (newline nil) ;We've seen newlines after the words.
+ (res nil)
+ prev)
+ (while (not res)
+ (setq prev (funcall smie-backward-token-function))
+ (cond
+ ((zerop (length prev))
+ (if newline
+ (progn (cl-assert words) (setq res 'word))
+ (setq words t)
+ (condition-case nil
+ (forward-sexp -1)
+ (scan-error (setq res 'unknown)))))
+ ((equal prev ";")
+ (if words (setq newline t)
+ (setq res 'keyword)))
+ ((member prev '("case" "for" "select")) (setq res 'keyword))
+ ((assoc prev smie-grammar) (setq res 'word))
+ (t
+ (if newline
+ (progn (cl-assert words) (setq res 'word))
+ (setq words t)))))
+ (eq res 'keyword)))
+
+(defun sh-smie--sh-keyword-p (tok)
+ "Non-nil if TOK (at which we're looking) really is a keyword."
+ (if (equal tok "in")
+ (sh-smie--sh-keyword-in-p)
+ (sh-smie--keyword-p tok)))
+
+(defun sh-smie-sh-forward-token ()
+ (if (and (looking-at "[ \t]*\\(?:#\\|\\(\\s|\\)\\|$\\)")
+ (save-excursion
+ (skip-chars-backward " \t")
+ (not (bolp))))
+ (if (and (match-end 1) (not (nth 3 (syntax-ppss))))
+ ;; Right before a here-doc.
+ (let ((forward-sexp-function nil))
+ (forward-sexp 1)
+ ;; Pretend the here-document is a "newline representing a
+ ;; semi-colon", since the here-doc otherwise covers the newline(s).
+ ";")
+ (let ((semi (sh-smie--newline-semi-p)))
+ (forward-line 1)
+ (if semi ";"
+ (sh-smie-sh-forward-token))))
+ (forward-comment (point-max))
+ (cond
+ ((looking-at "\\\\\n") (forward-line 1) (sh-smie-sh-forward-token))
+ ((looking-at sh-smie--sh-operators-re)
+ (goto-char (match-end 0))
+ (let ((tok (match-string-no-properties 0)))
+ (if (and (memq (aref tok (1- (length tok))) '(?\; ?\& ?\|))
+ (looking-at "[ \t]*\\(?:#\\|$\\)"))
+ (forward-line 1))
+ tok))
+ (t
+ (let* ((pos (point))
+ (tok (smie-default-forward-token)))
+ (cond
+ ((equal tok ")") "case-)")
+ ((equal tok "(") "case-(")
+ ((and tok (string-match "\\`[a-z]" tok)
+ (assoc tok smie-grammar)
+ (not
+ (save-excursion
+ (goto-char pos)
+ (sh-smie--sh-keyword-p tok))))
+ " word ")
+ (t tok)))))))
+
+(defun sh-smie--looking-back-at-continuation-p ()
+ (save-excursion
+ (and (if (eq (char-before) ?\n) (progn (forward-char -1) t) (eolp))
+ (looking-back "\\(?:^\\|[^\\]\\)\\(?:\\\\\\\\\\)*\\\\"
+ (line-beginning-position)))))
+
+(defun sh-smie-sh-backward-token ()
+ (let ((bol (line-beginning-position))
+ pos tok)
+ (forward-comment (- (point)))
+ (cond
+ ((and (bolp) (not (bobp))
+ (equal (syntax-after (1- (point))) (string-to-syntax "|"))
+ (not (nth 3 (syntax-ppss))))
+ ;; Right after a here-document.
+ (let ((forward-sexp-function nil))
+ (forward-sexp -1)
+ ;; Pretend the here-document is a "newline representing a
+ ;; semi-colon", since the here-doc otherwise covers the newline(s).
+ ";"))
+ ((< (point) bol)
+ (cond
+ ((sh-smie--looking-back-at-continuation-p)
+ (forward-char -1)
+ (funcall smie-backward-token-function))
+ ((sh-smie--newline-semi-p) ";")
+ (t (funcall smie-backward-token-function))))
+ ((looking-back sh-smie--sh-operators-back-re
+ (line-beginning-position) 'greedy)
+ (goto-char (match-beginning 1))
+ (match-string-no-properties 1))
+ (t
+ (let ((tok (smie-default-backward-token)))
+ (cond
+ ((equal tok ")") "case-)")
+ ((equal tok "(") "case-(")
+ ((and tok (string-match "\\`[a-z]" tok)
+ (assoc tok smie-grammar)
+ (not (save-excursion (sh-smie--sh-keyword-p tok))))
+ " word ")
+ (t tok)))))))
+
+(defcustom sh-indent-after-continuation t
+ "If non-nil, try to make sure text is indented after a line continuation."
+ :version "24.3"
+ :type 'boolean
+ :group 'sh-indentation)
+
+(defun sh-smie--continuation-start-indent ()
+ "Return the initial indentation of a continued line.
+May return nil if the line should not be treated as continued."
+ (save-excursion
+ (forward-line -1)
+ (unless (sh-smie--looking-back-at-continuation-p)
+ (current-indentation))))
+
+(defun sh-smie-sh-rules (kind token)
+ (pcase (cons kind token)
+ (`(:elem . basic) sh-indentation)
+ (`(:after . "case-)") (or sh-indentation smie-indent-basic))
+ ((and `(:before . ,_)
+ (guard (when sh-indent-after-continuation
+ (save-excursion
+ (ignore-errors
+ (skip-chars-backward " \t")
+ (sh-smie--looking-back-at-continuation-p))))))
+ ;; After a line-continuation, make sure the rest is indented.
+ (let* ((sh-indent-after-continuation nil)
+ (indent (smie-indent-calculate))
+ (initial (sh-smie--continuation-start-indent)))
+ (when (and (numberp indent) (numberp initial)
+ (<= indent initial))
+ `(column . ,(+ initial sh-indentation)))))
+ (`(:before . ,(or `"(" `"{" `"["))
+ (if (smie-rule-hanging-p) (smie-rule-parent)))
+ ;; FIXME: Maybe this handling of ;; should be made into
+ ;; a smie-rule-terminator function that takes the substitute ";" as arg.
+ (`(:before . ,(or `";;" `";&" `";;&"))
+ (if (and (smie-rule-bolp) (looking-at ";;?&?[ \t]*\\(#\\|$\\)"))
+ (cons 'column (smie-indent-keyword ";"))
+ (smie-rule-separator kind)))
+ (`(:after . ,(or `";;" `";&" `";;&"))
+ (with-demoted-errors
+ (smie-backward-sexp token)
+ (cons 'column
+ (if (or (smie-rule-bolp)
+ (save-excursion
+ (and (member (funcall smie-backward-token-function)
+ '("in" ";;"))
+ (smie-rule-bolp))))
+ (current-column)
+ (smie-indent-calculate)))))
+ (`(:after . "|") (if (smie-rule-parent-p "|") nil 4))
+ ))
+
+;; (defconst sh-smie-csh-grammar
+;; (smie-prec2->grammar
+;; (smie-bnf->prec2
+;; '((exp) ;A constant, or a $var, or a sequence of them…
+;; (elseifcmd (cmd)
+;; (cmd "else" "else-if" exp "then" elseifcmd))
+;; (cmd ("switch" branches "endsw")
+;; ("if" exp)
+;; ("if" exp "then" cmd "endif")
+;; ("if" exp "then" cmd "else" cmd "endif")
+;; ("if" exp "then" elseifcmd "endif")
+;; ;; ("if" exp "then" cmd "else" cmd "endif")
+;; ;; ("if" exp "then" cmd "else" "if" exp "then" cmd "endif")
+;; ;; ("if" exp "then" cmd "else" "if" exp "then" cmd
+;; ;; "else" cmd "endif")
+;; ;; ("if" exp "then" cmd "else" "if" exp "then" cmd
+;; ;; "else" "if" exp "then" cmd "endif")
+;; ("while" cmd "end")
+;; ("foreach" cmd "end")
+;; (cmd "|" cmd) (cmd "|&" cmd)
+;; (cmd "&&" cmd) (cmd "||" cmd)
+;; (cmd ";" cmd) (cmd "&" cmd))
+;; ;; This is a lie, but (combined with the corresponding disambiguation
+;; ;; rule) it makes it more clear that `case' and `default' are the key
+;; ;; separators and the `:' is a secondary tokens.
+;; (branches (branches "case" branches)
+;; (branches "default" branches)
+;; (exp ":" branches)))
+;; '((assoc "else" "then" "endif"))
+;; '((assoc "case" "default") (nonassoc ":"))
+;; '((assoc ";;" ";&" ";;&"))
+;; '((assoc ";" "&") (assoc "&&" "||") (assoc "|" "|&")))))
+
+;;;; SMIE support for `rc'.
+
+(defconst sh-smie-rc-grammar
+ (smie-prec2->grammar
+ (smie-bnf->prec2
+ '((exp) ;A constant, or a $var, or a sequence of them...
+ (cmd (cmd "case" cmd)
+ ("if" exp)
+ ("switch" exp)
+ ("for" exp) ("while" exp)
+ (cmd "|" cmd) (cmd "|&" cmd)
+ (cmd "&&" cmd) (cmd "||" cmd)
+ (cmd ";" cmd) (cmd "&" cmd))
+ (pattern (pattern "|" pattern))
+ (branches (branches ";;" branches)
+ (branches ";&" branches) (branches ";;&" branches) ;bash.
+ (pattern "case-)" cmd)))
+ '((assoc ";;" ";&" ";;&"))
+ '((assoc "case") (assoc ";" "&") (assoc "&&" "||") (assoc "|" "|&")))))
+
+(defun sh-smie--rc-after-special-arg-p ()
+ "Check if we're after the first arg of an if/while/for/... construct.
+Returns the construct's token and moves point before it, if so."
+ (forward-comment (- (point)))
+ (when (looking-back ")\\|\\_<not" (- (point) 3))
+ (ignore-errors
+ (let ((forward-sexp-function nil))
+ (forward-sexp -1)
+ (car (member (funcall smie-backward-token-function)
+ '("if" "for" "switch" "while")))))))
+
+(defun sh-smie--rc-newline-semi-p ()
+ "Return non-nil if a newline should be treated as a semi-colon.
+Point should be before the newline."
+ (save-excursion
+ (let ((tok (funcall smie-backward-token-function)))
+ (if (or (when (equal tok "not") (forward-word 1) t)
+ (and (zerop (length tok)) (eq (char-before) ?\))))
+ (not (sh-smie--rc-after-special-arg-p))
+ (sh-smie--newline-semi-p tok)))))
+
+(defun sh-smie-rc-forward-token ()
+ ;; FIXME: Code duplication with sh-smie-sh-forward-token.
+ (if (and (looking-at "[ \t]*\\(?:#\\|\\(\\s|\\)\\|$\\)")
+ (save-excursion
+ (skip-chars-backward " \t")
+ (not (bolp))))
+ (if (and (match-end 1) (not (nth 3 (syntax-ppss))))
+ ;; Right before a here-doc.
+ (let ((forward-sexp-function nil))
+ (forward-sexp 1)
+ ;; Pretend the here-document is a "newline representing a
+ ;; semi-colon", since the here-doc otherwise covers the newline(s).
+ ";")
+ (let ((semi (sh-smie--rc-newline-semi-p)))
+ (forward-line 1)
+ (if semi ";"
+ (sh-smie-rc-forward-token))))
+ (forward-comment (point-max))
+ (cond
+ ((looking-at "\\\\\n") (forward-line 1) (sh-smie-rc-forward-token))
+ ;; ((looking-at sh-smie--rc-operators-re)
+ ;; (goto-char (match-end 0))
+ ;; (let ((tok (match-string-no-properties 0)))
+ ;; (if (and (memq (aref tok (1- (length tok))) '(?\; ?\& ?\|))
+ ;; (looking-at "[ \t]*\\(?:#\\|$\\)"))
+ ;; (forward-line 1))
+ ;; tok))
+ (t
+ (let* ((pos (point))
+ (tok (smie-default-forward-token)))
+ (cond
+ ;; ((equal tok ")") "case-)")
+ ((and tok (string-match "\\`[a-z]" tok)
+ (assoc tok smie-grammar)
+ (not
+ (save-excursion
+ (goto-char pos)
+ (sh-smie--keyword-p tok))))
+ " word ")
+ (t tok)))))))
+
+(defun sh-smie-rc-backward-token ()
+ ;; FIXME: Code duplication with sh-smie-sh-backward-token.
+ (let ((bol (line-beginning-position))
+ pos tok)
+ (forward-comment (- (point)))
+ (cond
+ ((and (bolp) (not (bobp))
+ (equal (syntax-after (1- (point))) (string-to-syntax "|"))
+ (not (nth 3 (syntax-ppss))))
+ ;; Right after a here-document.
+ (let ((forward-sexp-function nil))
+ (forward-sexp -1)
+ ;; Pretend the here-document is a "newline representing a
+ ;; semi-colon", since the here-doc otherwise covers the newline(s).
+ ";"))
+ ((< (point) bol) ;We skipped over a newline.
+ (cond
+ ;; A continued line.
+ ((and (eolp)
+ (looking-back "\\(?:^\\|[^\\]\\)\\(?:\\\\\\\\\\)*\\\\"
+ (line-beginning-position)))
+ (forward-char -1)
+ (funcall smie-backward-token-function))
+ ((sh-smie--rc-newline-semi-p) ";")
+ (t (funcall smie-backward-token-function))))
+ ;; ((looking-back sh-smie--sh-operators-back-re
+ ;; (line-beginning-position) 'greedy)
+ ;; (goto-char (match-beginning 1))
+ ;; (match-string-no-properties 1))
+ (t
+ (let ((tok (smie-default-backward-token)))
+ (cond
+ ;; ((equal tok ")") "case-)")
+ ((and tok (string-match "\\`[a-z]" tok)
+ (assoc tok smie-grammar)
+ (not (save-excursion (sh-smie--keyword-p tok))))
+ " word ")
+ (t tok)))))))
+
+(defun sh-smie-rc-rules (kind token)
+ (pcase (cons kind token)
+ (`(:elem . basic) sh-indentation)
+ ;; (`(:after . "case") (or sh-indentation smie-indent-basic))
+ (`(:after . ";") (if (smie-rule-parent-p "case")
+ (smie-rule-parent sh-indentation)))
+ (`(:before . "{")
+ (save-excursion
+ (when (sh-smie--rc-after-special-arg-p)
+ `(column . ,(current-column)))))
+ (`(:before . ,(or `"(" `"{" `"["))
+ (if (smie-rule-hanging-p) (smie-rule-parent)))
+ ;; FIXME: SMIE parses "if (exp) cmd" as "(if ((exp) cmd))" so "cmd" is
+ ;; treated as an arg to (exp) by default, which indents it all wrong.
+ ;; To handle it right, we should extend smie-indent-exps so that the
+ ;; preceding keyword can give special rules. Currently the only special
+ ;; rule we have is the :list-intro hack, which we use here to align "cmd"
+ ;; with "(exp)", which is rarely the right thing to do, but is better
+ ;; than nothing.
+ (`(:list-intro . ,(or `"for" `"if" `"while")) t)
+ ))
+
+;;; End of SMIE code.
(defvar sh-regexp-for-done nil
"A buffer-local regexp to match opening keyword for done.")
(executable-set-magic shell (sh-feature sh-shell-arg)
no-query-flag insert-flag)))
(setq mode-line-process (format "[%s]" sh-shell))
- (set (make-local-variable 'sh-shell-variables) nil)
- (set (make-local-variable 'sh-shell-variables-initialized) nil)
- (set (make-local-variable 'imenu-generic-expression)
- (sh-feature sh-imenu-generic-expression))
+ (setq-local sh-shell-variables nil)
+ (setq-local sh-shell-variables-initialized nil)
+ (setq-local imenu-generic-expression
+ (sh-feature sh-imenu-generic-expression))
(let ((tem (sh-feature sh-mode-syntax-table-input)))
(when tem
- (set (make-local-variable 'sh-mode-syntax-table)
- (apply 'sh-mode-syntax-table tem))
+ (setq-local sh-mode-syntax-table
+ (apply 'sh-mode-syntax-table tem))
(set-syntax-table sh-mode-syntax-table)))
(dolist (var (sh-feature sh-variables))
(sh-remember-variable var))
- (if (setq sh-indent-supported-here (sh-feature sh-indent-supported))
+ (if (setq-local sh-indent-supported-here
+ (sh-feature sh-indent-supported))
(progn
(message "Setting up indent for shell type %s" sh-shell)
- (set (make-local-variable 'parse-sexp-lookup-properties) t)
- (set (make-local-variable 'sh-kw-alist) (sh-feature sh-kw))
- (let ((regexp (sh-feature sh-kws-for-done)))
- (if regexp
- (set (make-local-variable 'sh-regexp-for-done)
- (sh-mkword-regexpr (regexp-opt regexp t)))))
- (message "setting up indent stuff")
- ;; sh-mode has already made indent-line-function local
- ;; but do it in case this is called before that.
- (set (make-local-variable 'indent-line-function) 'sh-indent-line)
+ (if sh-use-smie
+ (let ((mksym (lambda (name)
+ (intern (format "sh-smie-%s-%s"
+ sh-indent-supported-here name)))))
+ (smie-setup (symbol-value (funcall mksym "grammar"))
+ (funcall mksym "rules")
+ :forward-token (funcall mksym "forward-token")
+ :backward-token (funcall mksym "backward-token")))
+ (setq-local parse-sexp-lookup-properties t)
+ (setq-local sh-kw-alist (sh-feature sh-kw))
+ (let ((regexp (sh-feature sh-kws-for-done)))
+ (if regexp
+ (setq-local sh-regexp-for-done
+ (sh-mkword-regexpr (regexp-opt regexp t)))))
+ (message "setting up indent stuff")
+ ;; sh-mode has already made indent-line-function local
+ ;; but do it in case this is called before that.
+ (setq-local indent-line-function 'sh-indent-line))
(if sh-make-vars-local
(sh-make-vars-local))
(message "Indentation setup for shell type %s" sh-shell))
\f
;; Indentation stuff.
(defun sh-must-support-indent ()
- "*Signal an error if the shell type for this buffer is not supported.
+ "Signal an error if the shell type for this buffer is not supported.
Also, the buffer must be in Shell-script mode."
(unless sh-indent-supported-here
(error "This buffer's shell does not support indentation through Emacs")))
;; Is this really worth having?
(defvar sh-learned-buffer-hook nil
- "*An abnormal hook, called with an alist of learned variables.")
+ "An abnormal hook, called with an alist of learned variables.")
;; Example of how to use sh-learned-buffer-hook
;;
;; (defun what-i-learned (list)
(defun sh-save-styles-to-buffer (buff)
"Save all current styles in elisp to buffer BUFF.
This is always added to the end of the buffer."
- (interactive (list
- (read-from-minibuffer "Buffer to save styles in? " "*scratch*")))
+ (interactive
+ (list
+ (read-from-minibuffer "Buffer to save styles in? " "*scratch*")))
(with-current-buffer (get-buffer-create buff)
(goto-char (point-max))
(insert "\n")
(defun sh-maybe-here-document (arg)
"Insert self. Without prefix, following unquoted `<' inserts here document.
The document is bounded by `sh-here-document-word'."
+ (declare (obsolete sh-electric-here-document-mode "24.3"))
(interactive "*P")
(self-insert-command (prefix-numeric-value arg))
- (or arg
- (not (looking-back "[^<]<<"))
+ (or arg (sh--maybe-here-document)))
+
+(defun sh--maybe-here-document ()
+ (or (not (looking-back "[^<]<<"))
(save-excursion
(backward-char 2)
(sh-quoted-p))
(insert ?\n tabs (replace-regexp-in-string
"\\`-?[ \t]*" "" delim))))))
+(define-minor-mode sh-electric-here-document-mode
+ "Make << insert a here document skeleton."
+ nil nil nil
+ (if sh-electric-here-document-mode
+ (add-hook 'post-self-insert-hook #'sh--maybe-here-document nil t)
+ (remove-hook 'post-self-insert-hook #'sh--maybe-here-document t)))
\f
;; various other commands
-(autoload 'comint-dynamic-complete "comint"
- "Dynamically perform completion at point." t)
-
-(autoload 'shell-dynamic-complete-command "shell"
- "Dynamically complete the command at point." t)
-
-(autoload 'comint-dynamic-complete-filename "comint"
- "Dynamically complete the filename at point." t)
-
-(autoload 'shell-dynamic-complete-environment-variable "shell"
- "Dynamically complete the environment variable at point." t)
-
-
-
(defun sh-beginning-of-command ()
+ ;; FIXME: Redefine using SMIE.
"Move point to successive beginnings of commands."
(interactive)
(if (re-search-backward sh-beginning-of-command nil t)
(goto-char (match-beginning 2))))
(defun sh-end-of-command ()
+ ;; FIXME: Redefine using SMIE.
"Move point to successive ends of commands."
(interactive)
(if (re-search-forward sh-end-of-command nil t)