]> code.delx.au - gnu-emacs/blobdiff - lisp/progmodes/sh-script.el
(sh-prev-thing): Remove (forward-char 1) now
[gnu-emacs] / lisp / progmodes / sh-script.el
index 6d4572a43f888651beda5b00cb448a8618cf841a..a15cc216f75ed2292b4e2fbe6091f841b52913ac 100644 (file)
@@ -1,7 +1,7 @@
 ;;; sh-script.el --- shell-script editing commands for Emacs
 
-;; Copyright (C) 1993, 1994, 1995, 1996, 1997, 1999, 2001, 2003, 2004, 2005
-;;  Free Software Foundation, Inc.
+;; Copyright (C) 1993, 1994, 1995, 1996, 1997, 1999, 2001, 2003, 2004, 2005,
+;;  2006  Free Software Foundation, Inc.
 
 ;; Author: Daniel Pfeiffer <occitan@esperanto.org>
 ;; Version: 2.0f
 
 (defgroup sh-script nil
   "Shell script mode."
+  :link '(custom-group-link :tag "Font Lock Faces group" font-lock-faces)
   :group 'sh
   :prefix "sh-")
 
@@ -355,6 +356,7 @@ the car and cdr are the same symbol.")
 
 (defvar sh-shell (sh-canonicalize-shell (file-name-nondirectory sh-shell-file))
   "The shell being programmed.  This is set by \\[sh-set-shell].")
+;;;###autoload(put 'sh-shell 'safe-local-variable 'symbolp)
 
 (defvar sh-mode-abbrev-table nil)
 
@@ -812,6 +814,18 @@ See `sh-feature'.")
      (:weight bold)))
   "Face to show a here-document"
   :group 'sh-indentation)
+
+;; These colours are probably icky.  It's just a placeholder though.
+(defface sh-quoted-exec
+  '((((class color) (background dark))
+     (:foreground "salmon"))
+    (((class color) (background light))
+     (:foreground "magenta"))
+    (t
+     (:weight bold)))
+  "Face to show quoted execs like ``"
+  :group 'sh-indentation)
+
 ;; backward-compatibility alias
 (put 'sh-heredoc-face 'face-alias 'sh-heredoc)
 (defvar sh-heredoc-face 'sh-heredoc)
@@ -831,7 +845,7 @@ See `sh-feature'.")
          font-lock-variable-name-face))
 
     (rc sh-append es)
-
+    (bash sh-append shell ("\\$(\\(\\sw+\\)" (1 'sh-quoted-exec t) ))
     (sh sh-append shell
        ;; Variable names.
        ("\\$\\({#?\\)?\\([A-Za-z_][A-Za-z0-9_]*\\|[-#?@!]\\)" 2
@@ -965,6 +979,59 @@ Point is at the beginning of the next line."
   ;; This looks silly, but it's because `sh-here-doc-re' keeps changing.
   (re-search-forward sh-here-doc-re limit t))
 
+(defun sh-quoted-subshell (limit)
+  "Search for a subshell embedded in a string.
+Find all the unescaped \" characters within said subshell, remembering that
+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.
+  (when (and (re-search-forward "\"\\(?:\\(?:.\\|\n\\)*?[^\\]\\(?:\\\\\\\\\\)*\\)??\\(\\$(\\|`\\)" limit t)
+             ;; Make sure the " we matched is an opening quote.
+            (eq ?\" (nth 3 (syntax-ppss))))
+    ;; bingo we have a $( or a ` inside a ""
+    (let ((char (char-after (point)))
+          (continue t)
+          (pos (point))
+          (data nil)      ;; value to put into match-data (and return)
+          (last nil)      ;; last char seen
+          (bq  (equal (match-string 1) "`")) ;; ` state flip-flop
+          (seen nil)                         ;; list of important positions
+          (nest 1))                          ;; subshell nesting level
+      (while (and continue char (<= pos limit))
+        ;; unescaped " inside a $( ... ) construct.
+        ;; state machine time...
+        ;; \ => ignore next char;
+        ;; ` => increase or decrease nesting level based on bq flag
+        ;; ) [where nesting > 0] => decrease nesting
+        ;; ( [where nesting > 0] => increase nesting
+        ;; ( [preceeded by $ ]   => increase nesting
+        ;; " [nesting <= 0 ]     => terminate, we're done.
+        ;; " [nesting >  0 ]     => remember this, it's not a proper "
+        ;; FIXME: don't count parens that appear within quotes.
+        (cond
+         ((eq ?\\ last) nil)
+         ((eq ?\` char) (setq nest (+ nest (if bq -1 1)) bq (not bq)))
+         ((and (> nest 0) (eq ?\) char))   (setq nest (1- nest)))
+         ((and (eq ?$ last) (eq ?\( char)) (setq nest (1+ nest)))
+         ((and (> nest 0) (eq ?\( char))   (setq nest (1+ nest)))
+         ((eq char ?\")
+          (if (>= 0 nest) (setq continue nil) (push pos seen))))
+        ;;(message "POS: %d [%d]" pos nest)
+        (setq last char
+              pos  (1+ pos)
+              char (char-after pos)) )
+      ;; FIXME: why construct a costly match data to pass to
+      ;; sh-apply-quoted-subshell rather than apply the highlight
+      ;; directly here?  -- Stef
+      (when seen
+        ;;(message "SEEN: %S" seen)
+        (setq data (list (current-buffer)))
+        (dolist(P seen)
+          (setq data (cons P (cons (1+ P) data))))
+        (store-match-data data))
+      data) ))
+
 (defun sh-is-quoted-p (pos)
   (and (eq (char-before pos) ?\\)
        (not (sh-is-quoted-p (1- pos)))))
@@ -985,18 +1052,37 @@ Point is at the beginning of the next line."
                  (when (memq (char-before) '(?\" ?\'))
                    (condition-case nil (progn (backward-sexp 1) t)
                      (error nil)))))
-         (forward-comment (- (point-max)))
+          (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 (save-excursion (backward-char 2) (looking-at ";;\\|in"))
       sh-st-punc)))
 
+(defun sh-apply-quoted-subshell ()
+  "Apply the `sh-st-punc' syntax to all the matches in `match-data'.
+This is used to flag quote characters in subshell constructs inside strings
+\(which should therefore not be treated as normal quote characters\)"
+  (let ((m (match-data)) a b)
+    (while m
+      (setq a (car  m)
+            b (cadr m)
+            m (cddr m))
+      (put-text-property a b 'syntax-table sh-st-punc))) sh-st-punc)
+
 (defconst sh-font-lock-syntactic-keywords
   ;; 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 ,sh-st-symbol)
+    ;; In a '...' the backslash is not escaping.
+    ("\\(\\\\\\)'" 1 ,sh-st-punc)
+    ;; Make sure $@ and @? are correctly recognized as sexps.
+    ("\\$\\([?@]\\)" 1 ,sh-st-symbol)
     ;; Find HEREDOC starters and add a corresponding rule for the ender.
     (sh-font-lock-here-doc
      (2 (sh-font-lock-open-heredoc
@@ -1006,14 +1092,19 @@ Point is at the beginning of the next line."
          (and (match-beginning 3) (/= (match-beginning 3) (match-end 3))))
       nil t))
     ;; Distinguish the special close-paren in `case'.
-    (")" 0 (sh-font-lock-paren (match-beginning 0)))))
+    (")" 0 (sh-font-lock-paren (match-beginning 0)))
+    ;; highlight (possibly nested) subshells inside "" quoted regions correctly.
+    ;; This should be at the very end because it uses syntax-ppss.
+    (sh-quoted-subshell
+     (1 (sh-apply-quoted-subshell) t t))))
 
 (defun sh-font-lock-syntactic-face-function (state)
-  (if (nth 3 state)
-      (if (char-valid-p (nth 3 state))
-         font-lock-string-face
-       sh-heredoc-face)
-    font-lock-comment-face))
+  (let ((q (nth 3 state)))
+    (if q
+        (if (char-valid-p q)
+            (if (eq q ?\`) 'sh-quoted-exec font-lock-string-face)
+          sh-heredoc-face)
+      font-lock-comment-face)))
 
 (defgroup sh-indentation nil
   "Variables controlling indentation in shell scripts.
@@ -1327,7 +1418,7 @@ shell-specific features.
 The default style of this mode is that of Rosenblatt's Korn shell book.
 The syntax of the statements varies with the shell being used.  The
 following commands are available, based on the current shell's syntax:
-
+\\<sh-mode-map>
 \\[sh-case]     case statement
 \\[sh-for]      for loop
 \\[sh-function]         function definition
@@ -1380,11 +1471,11 @@ with your script for an edit-interpret-debug cycle."
   (make-local-variable 'sh-shell-file)
   (make-local-variable 'sh-shell)
   (make-local-variable 'skeleton-pair-alist)
-  (make-local-variable 'skeleton-pair-filter)
+  (make-local-variable 'skeleton-pair-filter-function)
   (make-local-variable 'comint-dynamic-complete-functions)
   (make-local-variable 'comint-prompt-regexp)
   (make-local-variable 'font-lock-defaults)
-  (make-local-variable 'skeleton-filter)
+  (make-local-variable 'skeleton-filter-function)
   (make-local-variable 'skeleton-newline-indent-rigidly)
   (make-local-variable 'sh-shell-variables)
   (make-local-variable 'sh-shell-variables-initialized)
@@ -1412,24 +1503,36 @@ with your script for an edit-interpret-debug cycle."
          (font-lock-syntactic-face-function
           . sh-font-lock-syntactic-face-function))
        skeleton-pair-alist '((?` _ ?`))
-       skeleton-pair-filter 'sh-quoted-p
+       skeleton-pair-filter-function 'sh-quoted-p
        skeleton-further-elements '((< '(- (min sh-indentation
                                                (current-column)))))
-       skeleton-filter 'sh-feature
+       skeleton-filter-function 'sh-feature
        skeleton-newline-indent-rigidly t
        sh-indent-supported-here nil)
   (set (make-local-variable 'parse-sexp-ignore-comments) t)
   ;; Parse or insert magic number for exec, and set all variables depending
   ;; on the shell thus determined.
-  (let ((interpreter
-        (save-excursion
-          (goto-char (point-min))
-          (cond ((looking-at "#![ \t]?\\([^ \t\n]*/bin/env[ \t]\\)?\\([^ \t\n]+\\)")
-                 (match-string 2))
-                ((and buffer-file-name
-                      (string-match "\\.m?spec\\'" buffer-file-name))
-                 "rpm")))))
-    (sh-set-shell (or interpreter sh-shell-file) nil nil))
+  (sh-set-shell
+   (cond ((save-excursion
+            (goto-char (point-min))
+            (looking-at "#![ \t]?\\([^ \t\n]*/bin/env[ \t]\\)?\\([^ \t\n]+\\)"))
+          (match-string 2))
+         ((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")
+         (t
+          sh-shell-file))
+   nil nil)
   (run-mode-hooks 'sh-mode-hook))
 
 ;;;###autoload
@@ -2357,46 +2460,45 @@ we go to the end of the previous line and do not check for continuations."
   ;;
   (if (bolp)
       nil
-    (let (c min-point
-         (start (point)))
-      (save-restriction
-       (narrow-to-region
-       (if (sh-this-is-a-continuation)
-           (setq min-point (sh-prev-line nil))
-         (save-excursion
-           (beginning-of-line)
-           (setq min-point (point))))
-       (point))
-       (skip-chars-backward " \t;")
-       (unless (looking-at "\\s-*;;")
-       (skip-chars-backward "^)}];\"'`({[")
-       (setq c (char-before))))
-      (sh-debug "stopping at %d c is %s  start=%d min-point=%d"
-               (point) c start min-point)
-      (if (< (point) min-point)
-         (error "point %d < min-point %d" (point) min-point))
-      (cond
-       ((looking-at "\\s-*;;")
-       ;; (message "Found ;; !")
-       ";;")
-       ((or (eq c ?\n)
-           (eq c nil)
-           (eq c ?\;))
-       (save-excursion
-        ;; skip forward over white space newline and \ at eol
-        (skip-chars-forward " \t\n\\\\")
-        (sh-debug "Now at %d   start=%d" (point) start)
-        (if (>= (point) start)
-            (progn
-              (sh-debug "point: %d >= start: %d" (point) start)
-              nil)
-          (sh-get-word))
-        ))
-       (t
-       ;; c    -- return a string
-       (char-to-string c)
-       ))
-      )))
+    (let ((start (point))
+          (min-point (if (sh-this-is-a-continuation)
+                         (sh-prev-line nil)
+                       (line-beginning-position))))
+      (skip-chars-backward " \t;" min-point)
+      (if (looking-at "\\s-*;;")
+          ;; (message "Found ;; !")
+          ";;"
+        (skip-chars-backward "^)}];\"'`({[" min-point)
+        (let ((c (if (> (point) min-point) (char-before))))
+          (sh-debug "stopping at %d c is %s  start=%d min-point=%d"
+                    (point) c start min-point)
+          (if (not (memq c '(?\n nil ?\;)))
+              ;; c     -- return a string
+              (char-to-string c)
+            ;; Return the leading keyword of the "command" we supposedly
+            ;; skipped over.  Maybe we skipped too far (e.g. past a `do' or
+            ;; `then' that precedes the actual command), so check whether
+            ;; we're looking at such a keyword and if so, move back forward.
+            (let ((boundary (point))
+                  kwd next)
+              (while
+                  (progn
+                    ;; Skip forward over white space newline and \ at eol.
+                    (skip-chars-forward " \t\n\\\\" start)
+                    (if (>= (point) start)
+                        (progn
+                          (sh-debug "point: %d >= start: %d" (point) start)
+                          nil)
+                      (if next (setq boundary next))
+                      (sh-debug "Now at %d   start=%d" (point) start)
+                      (setq kwd (sh-get-word))
+                      (if (member kwd (sh-feature sh-leading-keywords))
+                          (progn
+                            (setq next (point))
+                            t)
+                        nil))))
+              (goto-char boundary)
+              kwd)))))))
 
 
 (defun sh-this-is-a-continuation ()