13

In vim, the following document will cause the $PWD on lines 2 and 3 to be colored in two different ways:

#/bin/sh
echo "Current Directory: $PWD"
echo 'Current Directory: $PWD'

The first instance of $PWD will be in a different color from the rest of the string it is in. This gives a clear visual indication that the variable will be expanded, rather than treated as literal text. By contrast, the second instance of $PWD will be colored the same as the rest of the string, because the single quotes cause it to be treated as literal text.

Are there any existing emacs modes which provide this type of "shell-quoting awareness?"

  • 1
    Surely, this wouldn't be terribly hard to add to sh-mode? Maybe it can be added to Emacs itself. – PythonNut Jun 13 '15 at 1:31
11

The code below use a font-lock rule with a function instead of a regexp, the function search for occurrences of $VAR but only when they are inside a double-quoted string. The function (syntax-ppss) is used to determine this.

The font-lock rule use the prepend flag to add itself on top of the existing string highlighting. (Note that many packages use t for this. Unfortunately, this overwrites all aspects of the existing highlighting. For example, using prepend will retain a string background color (if there is one) while replacing the foreground color.)

(defun sh-script-extra-font-lock-is-in-double-quoted-string ()
  "Non-nil if point in inside a double-quoted string."
  (let ((state (syntax-ppss)))
    (eq (nth 3 state) ?\")))

(defun sh-script-extra-font-lock-match-var-in-double-quoted-string (limit)
  "Search for variables in double-quoted strings."
  (let (res)
    (while
        (and (setq res
                   (re-search-forward
                    "\\$\\({#?\\)?\\([[:alpha:]_][[:alnum:]_]*\\|[-#?@!]\\)"
                    limit t))
             (not (sh-script-extra-font-lock-is-in-double-quoted-string))))
    res))

(defvar sh-script-extra-font-lock-keywords
  '((sh-script-extra-font-lock-match-var-in-double-quoted-string
     (2 font-lock-variable-name-face prepend))))

(defun sh-script-extra-font-lock-activate ()
  (interactive)
  (font-lock-add-keywords nil sh-script-extra-font-lock-keywords)
  (if (fboundp 'font-lock-flush)
      (font-lock-flush)
    (when font-lock-mode
      (with-no-warnings
        (font-lock-fontify-buffer)))))

You can call use this by adding the last function to a suitable hook, for example:

(add-hook 'sh-mode-hook 'sh-script-extra-font-lock-activate)
  • This works for me, but leaves the "$" with the string highlighting. – erikstokes Jun 24 '15 at 23:32
  • It's by design, since that it how a variable outside a string was highlighted. However, this can easily be changed. If you replace the 2 in the font-lock rule with a 0 it should work. (You might need to extend the regexp to include a trailing } to highlight ${FOO} properly.) This number refers to the regexp subgroup of the match, 0 means that the entire match should be highlighted. – Lindydancer Jun 25 '15 at 4:05
  • Packaged this up while adding this to my .emacs.d repository if anyones interested: github.com/moonlite/.emacs.d/blob/… @Lindydancer Is GPLv3+ and you as author in that file fine? (I'll push an update if not). – Mattias Bengtsson Jun 27 '15 at 17:43
  • Sounds good. I probably wouldn't have to time to make it into a proper package. However, I would like you to drop my email address and instead add a line to my EmacsWiki page (emacswiki.org/emacs/AndersLindgren). Also, you can remove the copyright sign as it isn't necessary and it makes the source code non-ascii. – Lindydancer Jun 28 '15 at 9:16
3

I improved @Lindydancer's answer in the following ways:

  • Inlined the sh-script-extra-font-lock-is-in-double-quoted-string function, as it was only used once
  • Escaping the variable works.
  • Numeric variables ($10, $1, etc) are highlighted.

Break for code

(defun sh-script-extra-font-lock-match-var-in-double-quoted-string (limit)
  "Search for variables in double-quoted strings."
  (let (res)
    (while
        (and (setq res (progn (if (eq (get-byte) ?$) (backward-char))
                              (re-search-forward
                               "[^\\]\\$\\({#?\\)?\\([[:alpha:]_][[:alnum:]_]*\\|[-#?@!]\\|[[:digit:]]+\\)"
                               limit t)))
             (not (eq (nth 3 (syntax-ppss)) ?\")))) res))

(defvar sh-script-extra-font-lock-keywords
  '((sh-script-extra-font-lock-match-var-in-double-quoted-string
     (2 font-lock-variable-name-face prepend))))

(defun sh-script-extra-font-lock-activate ()
  (interactive)
  (font-lock-add-keywords nil sh-script-extra-font-lock-keywords)
  (if (fboundp 'font-lock-flush)
      (font-lock-flush)
    (when font-lock-mode (with-no-warnings (font-lock-fontify-buffer)))))
  • The [^\\\\] could be written as [^\\], it's a set of characters that should not be matched, and your code includes backslash twice. In older Emacs versions must use font-lock-fontify-buffer, in newer you are supposed to call font-lock-flush and calling font-lock-fontify-buffer from elisp is deprecated. My original code followed this, your code doesn't. Anyway, it might be a better idea to migrate this to a GitHub archive and join the effort. – Lindydancer Aug 15 '15 at 7:57
  • @Lindydancer Doesn't [^\\] escape the ]? That's how regex works in Java as I know. – Czipperz Aug 15 '15 at 22:39
  • @Lindydancer Seems like it doesn't as ELisp doesn't allow you to use escape characters in character groups. – Czipperz Aug 15 '15 at 22:45
  • 1

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.