15

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?"

Dan
  • 32,584
  • 6
  • 98
  • 168
nispio
  • 8,175
  • 2
  • 35
  • 73

3 Answers3

13

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)
Lindydancer
  • 6,095
  • 1
  • 13
  • 25
  • 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 04:05
  • Packaged this up while adding this to my .emacs.d repository if anyones interested: https://github.com/moonlite/.emacs.d/blob/f784fa96b17f8ab214e6c2ff08e317057deef133/packages/sh-extra-font-lock/sh-extra-font-lock.el @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 (http://www.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 09:16
4

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)))))
Czipperz
  • 317
  • 2
  • 9
  • 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 07: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
    https://github.com/czipperz/highlight-quoted-vars.el – Czipperz Aug 15 '15 at 23:10
0

I was stuck on vim because of its nice syntax highlighting in shell, finally, I got better syntax highlighting (including for the case of this question: highlighitng shell variables within quotes) than the one I had in vanilla vim/neovim thanks to the emacslisp tree-sitter package

What's tree-sitter? In the following video you can see tree-sitter in action, you don't need to wait to be on core at emacs 29.x stable release; you can use it now (requires Emacs 25.1 or above), check out installation instructions (below). EmacsConf 2022: Tree-sitter beyond syntax highlighting - Abin Simon https://www.youtube.com/watch?v=MZPR_SC9LzE

I copy here the configuration that I use, check installation link for other installation methods

;; https://emacs-tree-sitter.github.io/installation/
(use-package tree-sitter :ensure t)
(use-package tree-sitter-langs :ensure t)
;; https://emacs-tree-sitter.github.io/getting-started/
;; https://emacs-tree-sitter.github.io/syntax-highlighting/
;; To enable it whenever possible (assuming the language major modes were already installed):
(global-tree-sitter-mode)
(add-hook 'tree-sitter-after-on-hook #'tree-sitter-hl-mode)
bladem
  • 21
  • 4
  • How does this answer the question? – Drew Dec 30 '22 at 03:30
  • Hi @Drew, due to your comment, I edited my response with: "including for the case of this question: highlighting shell variables within quotes". I arrived to this question looking for a better syntax highlighting, of course, the most relevant problem is this one; but still, there are other problems with shell syntax highlightining; and I solved a lot of them with tree-sitter. The solutions posted here are very old, yea, they work, but are snippets that are not part of a package and so on; that's why I see the solution I found is better. I posted it here so other people get the benefit :) – bladem Dec 31 '22 at 11:41