;;; 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-2012 Free Software Foundation, Inc.
;; Author: Daniel Pfeiffer <occitan@esperanto.org>
;; Version: 2.0f
(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)
'((csh . "\\<\\([[:alnum:]_]+\\)\\(\\[.+\\]\\)?[ \t]*[-+*/%^]?=")
;; actually spaces are only supported in let/(( ... ))
(ksh88 . "\\<\\([[: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
(defconst sh-st-punc (string-to-syntax "."))
(defconst sh-here-doc-syntax (string-to-syntax "|")) ;; generic string
-(defconst sh-escaped-line-re
- ;; Should match until the real end-of-continued-line, but if that is not
- ;; possible (because we bump into EOB or the search bound), then we should
- ;; match until the search bound.
- "\\(?:\\(?:.*[^\\\n]\\)?\\(?:\\\\\\\\\\)*\\\\\n\\)*.*")
-
-(defconst sh-here-doc-open-re
- (concat "<<-?\\s-*\\\\?\\(\\(?:['\"][^'\"]+['\"]\\|\\sw\\)+\\)"
- sh-escaped-line-re "\\(\n\\)"))
-
-(defvar sh-here-doc-markers nil)
-(make-variable-buffer-local 'sh-here-doc-markers)
-(defvar sh-here-doc-re sh-here-doc-open-re)
-(make-variable-buffer-local 'sh-here-doc-re)
-
-(defun sh-font-lock-close-heredoc (bol eof indented eol)
- "Determine the syntax of the \\n after an EOF.
-If non-nil INDENTED indicates that the EOF was indented."
- (let* ((eof-re (if eof (regexp-quote eof) ""))
- ;; A rough regexp that should find the opening <<EOF back.
- (sre (concat "<<\\(-?\\)\\s-*['\"\\]?"
- ;; Use \s| to cheaply check it's an open-heredoc.
- eof-re "['\"]?\\([ \t|;&)<>]"
- sh-escaped-line-re
- "\\)?\\s|"))
- ;; A regexp that will find other EOFs.
- (ere (concat "^" (if indented "[ \t]*") eof-re "\n"))
- (start (save-excursion
- (goto-char bol)
- ;; FIXME: will incorrectly find a <<EOF embedded inside
- ;; the heredoc.
- (re-search-backward (concat sre "\\|" ere) nil t))))
- ;; If subgroup 1 matched, we found an open-heredoc, otherwise we first
- ;; found a close-heredoc which makes the current close-heredoc inoperant.
- (cond
- ((when (and start (match-end 1)
- (not (and indented (= (match-beginning 1) (match-end 1))))
- (not (sh-in-comment-or-string (match-beginning 0))))
- ;; Make sure our `<<' is not the EOF1 of a `cat <<EOF1 <<EOF2'.
- (save-excursion
- (goto-char start)
- (setq start (line-beginning-position 2))
- (while
- (progn
- (re-search-forward "<<") ; Skip ourselves.
- (and (re-search-forward sh-here-doc-open-re start 'move)
- (goto-char (match-beginning 0))
- (sh-in-comment-or-string (point)))))
- ;; No <<EOF2 found after our <<.
- (= (point) start)))
- (put-text-property eol (1+ eol) 'syntax-table sh-here-doc-syntax))
- ((not (or start (save-excursion (re-search-forward sre nil t))))
- ;; There's no <<EOF either before or after us,
- ;; so we should remove ourselves from font-lock's keywords.
- (setq sh-here-doc-markers (delete eof sh-here-doc-markers))
- (setq sh-here-doc-re
- (concat sh-here-doc-open-re "\\|^\\([ \t]*\\)"
- (regexp-opt sh-here-doc-markers t) "\\(\n\\)"))
- nil))))
+(eval-and-compile
+ (defconst sh-escaped-line-re
+ ;; Should match until the real end-of-continued-line, but if that is not
+ ;; possible (because we bump into EOB or the search bound), then we should
+ ;; match until the search bound.
+ "\\(?:\\(?:.*[^\\\n]\\)?\\(?:\\\\\\\\\\)*\\\\\n\\)*.*")
+
+ (defconst sh-here-doc-open-re
+ (concat "<<-?\\s-*\\\\?\\(\\(?:['\"][^'\"]+['\"]\\|\\sw\\|[-/~._]\\)+\\)"
+ sh-escaped-line-re "\\(\n\\)")))
(defun sh-font-lock-open-heredoc (start string eol)
"Determine the syntax of the \\n after a <<EOF.
(sh-in-comment-or-string 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)))
- (unless (member str sh-here-doc-markers)
- (push str sh-here-doc-markers)
- (setq sh-here-doc-re
- (concat sh-here-doc-open-re "\\|^\\([ \t]*\\)"
- (regexp-opt sh-here-doc-markers t) "\\(\n\\)"))))
- (let ((ppss (save-excursion (syntax-ppss (1- (point))))))
+ (let ((str (replace-regexp-in-string "['\"]" "" string))
+ (ppss (save-excursion (syntax-ppss eol))))
(if (nth 4 ppss)
;; The \n not only starts the heredoc but also closes a comment.
;; Let's close the comment just before the \n.
- (put-text-property (1- (point)) (point) 'syntax-table '(12))) ;">"
- (if (or (nth 5 ppss) (> (count-lines start (point)) 1))
- ;; If the sh-escaped-line-re part of sh-here-doc-re has matched
+ (put-text-property (1- eol) eol 'syntax-table '(12))) ;">"
+ (if (or (nth 5 ppss) (> (count-lines start eol) 1))
+ ;; If the sh-escaped-line-re part of sh-here-doc-open-re has matched
;; several lines, make sure we refontify them together.
;; Furthermore, if (nth 5 ppss) is non-nil (i.e. the \n is
;; escaped), it means the right \n is actually further down.
;; Don't bother fixing it now, but place a multiline property so
;; that when jit-lock-context-* refontifies the rest of the
;; buffer, it also refontifies the current line with it.
- (put-text-property start (point) 'syntax-multiline t)))
- (put-text-property eol (1+ eol) 'syntax-table sh-here-doc-syntax)))
+ (put-text-property start (1+ eol) 'syntax-multiline t))
+ (put-text-property eol (1+ eol) 'sh-here-doc-marker str)
+ (prog1 sh-here-doc-syntax
+ (goto-char (+ 2 start))))))
+
+(defun sh-syntax-propertize-here-doc (end)
+ (let ((ppss (syntax-ppss)))
+ (when (eq t (nth 3 ppss))
+ (let ((key (get-text-property (nth 8 ppss) 'sh-here-doc-marker))
+ (case-fold-search nil))
+ (when (re-search-forward
+ (concat "^\\([ \t]*\\)" (regexp-quote key) "\\(\n\\)")
+ end 'move)
+ (let ((eol (match-beginning 2)))
+ (put-text-property eol (1+ eol)
+ 'syntax-table sh-here-doc-syntax)))))))
(defun sh-font-lock-quoted-subshell (limit)
"Search for a subshell embedded in a 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 ((char (char-after (point)))
- ;; `state' can be: double-quote, backquote, code.
+ (let (;; `state' can be: double-quote, backquote, code.
(state (if (eq (char-before) ?`) 'backquote 'code))
;; Stacked states in the context.
(states '(double-quote)))
(not (sh-is-quoted-p (1- pos)))))
(defun sh-font-lock-paren (start)
- (save-excursion
- (goto-char start)
- ;; Skip through all patterns
- (while
- (progn
- (forward-comment (- (point-max)))
- ;; 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)) ?\()
- (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)))
- ;; FIXME: ";; 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.
- (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'.
- (looking-at ";;\\|in"))
- sh-st-punc)))
+ (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)) ?\()
+ (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))))
(defun sh-font-lock-backslash-quote ()
(if (eq (save-excursion (nth 3 (syntax-ppss (match-beginning 0)))) ?\')
(defun sh-syntax-propertize-function (start end)
(goto-char start)
- (while (prog1
- (re-search-forward sh-here-doc-re end 'move)
+ (sh-syntax-propertize-here-doc end)
+ (funcall
+ (syntax-propertize-rules
+ (sh-here-doc-open-re
+ (2 (sh-font-lock-open-heredoc
+ (match-beginning 0) (match-string 1) (match-beginning 2))))
+ ("\\s|" (0 (prog1 nil (sh-syntax-propertize-here-doc end))))
+ ;; A `#' begins a comment when it is unquoted and at the
+ ;; beginning of a word. In the shell, words are separated by
+ ;; 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 "_"))
+ ;; In a '...' the backslash is not escaping.
+ ("\\(\\\\\\)'" (1 (sh-font-lock-backslash-quote)))
+ ;; Make sure $@ and $? are correctly recognized as sexps.
+ ("\\$\\([?@]\\)" (1 "_"))
+ ;; Distinguish the special close-paren in `case'.
+ (")" (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))
+ (goto-char (1+ (match-beginning 0)))
(save-excursion
- (save-match-data
- (funcall
- (syntax-propertize-rules
- ;; A `#' begins a comment when it is unquoted and at the
- ;; beginning of a word. In the shell, words are separated by
- ;; 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 "_"))
- ;; In a '...' the backslash is not escaping.
- ("\\(\\\\\\)'" (1 (sh-font-lock-backslash-quote)))
- ;; Make sure $@ and $? are correctly recognized as sexps.
- ("\\$\\([?@]\\)" (1 "_"))
- ;; Distinguish the special close-paren in `case'.
- (")" (0 (sh-font-lock-paren (match-beginning 0))))
- ;; Highlight (possibly nested) subshells inside "" quoted
- ;; regions correctly.
- ("\"\\(?:\\(?:.\\|\n\\)*?[^\\]\\(?:\\\\\\\\\\)*\\)??\\(\\$(\\|`\\)"
- (1 (ignore
- ;; Save excursion because we want to also apply other
- ;; syntax-propertize rules within the affected region.
- (save-excursion
- (sh-font-lock-quoted-subshell end))))))
- (prog1 start (setq start (point))) (point)))))
- (if (match-beginning 2)
- ;; FIXME: actually, once we see an heredoc opener, we should just
- ;; search for its ender without propertizing anything in it.
- (sh-font-lock-open-heredoc
- (match-beginning 0) (match-string 1) (match-beginning 2))
- (sh-font-lock-close-heredoc
- (match-beginning 0) (match-string 4)
- (and (match-beginning 3) (/= (match-beginning 3) (match-end 3)))
- (match-beginning 5)))))
+ (sh-font-lock-quoted-subshell end)))))))
+ (point) end))
(defun sh-font-lock-syntactic-face-function (state)
(let ((q (nth 3 state)))
;; "For debugging: display message ARGS if variable SH-DEBUG is non-nil."
;; (if sh-debug
;; (apply 'message args)))
-(defmacro sh-debug (&rest args))
+(defmacro sh-debug (&rest _args))
(defconst sh-symbol-list
'((const :tag "+ " :value +
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'.
("esac" sh-handle-this-esac sh-handle-prev-esac)
(case-label nil sh-handle-after-case-label) ;; ???
(";;" nil sh-handle-prev-case-alt-end) ;; ???
+ (";;&" nil sh-handle-prev-case-alt-end) ;Like ";;" with diff semantics.
+ (";&" nil sh-handle-prev-case-alt-end) ;Like ";;" with diff semantics.
("done" sh-handle-this-done sh-handle-prev-done)
("do" sh-handle-this-do sh-handle-prev-do))
\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")))
(save-excursion
(let ((have-result nil)
this-kw
- start
val
(result nil)
(align-point nil)
;; We start off at beginning of this line.
;; Scan previous statements while this is <=
;; start of previous line.
- (setq start (point)) ;; for debug only
(goto-char prev-line-end)
(setq x t)
(while (and x (setq x (sh-prev-thing)))
(sh-prev-line nil)
(line-beginning-position))))
(skip-chars-backward " \t;" min-point)
- (if (looking-at "\\s-*;;")
+ (if (looking-at "\\s-*;[;&]")
;; (message "Found ;; !")
";;"
(skip-chars-backward "^)}];\"'`({[" min-point)
If INFO is supplied it is used, else it is calculated from current line."
(let ((ofs 0)
(base-value 0)
- elt a b var val)
+ elt a b val)
(or info
(setq info (sh-get-indent-info)))
(when info
;; 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)
(save-excursion
(backward-char 2)
(sh-quoted-p))
+ (nth 8 (syntax-ppss))
(let ((tabs (if (string-match "\\`-" sh-here-document-word)
(make-string (/ (current-indentation) tab-width) ?\t)
""))