4

Say you're coding a shell script and you want to write something like:

  if [[ "$out" =~ 'none' ]]; then
    return $err
  fi

If you're just typing it out and have any of the various electric/pair modules active, you'll likely get something like

if [[‸]]

(with the position of point indicated by ).

In most programming languages, this is great, but in POSIX/Korn-family shells like Bash or Zsh, you pretty much always should have a space after the [[ and before the ]].

Inserting this manually is a pain since you need a space before, and a space after, and still get the point back in the middle. I frequently have wild error messages the first time i test a script because one of those spaces is missing, or they both ended up before or after the double-brace contents.

Is there a good way to rectify this, so that, ideally, in sh-script-mode, typing [[ would result in

[[ ‸ ]]

so the necessary space is there and the point is correctly positioned?

Ideally, the answer would also let me select a region and press [[, and just as I can already today surround the region with brackets, parens, etc. this way, I'd get the same, just with [[ $region ]] instead of [[$region]]. Also, it would be useful if the Zsh (( ... )) form would act in the same way.

(Note that I specifically do not want this behavior for square brackets in general; they're too-frequently used in other contexts when one doesn't want extraneous spaces, and the use of the single-bracket [ ... ] test construct is generally less preferable to the double-bracket one.)


p.s.

I'm setting aside YAS snippets and such for the moment—they could clearly solve this problem in many cases, and I even already use them to create entire if..elif..else..fi blocks with built-in [[ $0 ]]-type positioning code. And so, for writing entirely new shell code, this problem isn't that big of a deal.

But this issue tends to arise when I'm modifying existing logic, rather than entering in entirely new conditionals. One of the most common ways one might need to fix a wayward conditional is to change a simple conditional into a test, so introducing the [[ ... ]] test into an existing conditional is a frequent task.

An acceptable answer, but one I've tried to avoid, would be to make [[ a YAS snippet in its own right. The issue is that if I defined it such that invocation was [[⇥ ( meaning the tab or other YAS invocation key), the doubling expansion will have already happened, so I'd have to have Elisp in the snippet to fix the characters after the snippet's environment, which is something of a pain.

1 Answer 1

3

You could try smartparens. With the settings below:

Pressing [ SPC becomes [ | ]
Pressing [ [ SPC becomes [[ | ]]
Selecting foo and pressing [ becomes [ foo ]
Pressing [ a second time becomes [[ foo ]]

(require 'smartparens)
(smartparens-global-mode 1)
(show-paren-mode 1)

(defun sp-shell-insert-spaces (_id action _context)
  (when (eq action 'wrap)
    (save-excursion
      (goto-char (sp-get sp-last-wrapped-region :end-in))
      (unless (looking-at "]]")
        (insert " "))
      (goto-char (sp-get sp-last-wrapped-region :beg-in))
      (unless (looking-back "\\[\\[" nil)
        (insert " ")))))

(sp-with-modes 'sh-mode
  (sp-local-pair "[" "]" :actions '(wrap insert navigate) :post-handlers '(sp-shell-insert-spaces))
  (sp-local-pair "[ " " ]" :actions '(wrap insert navigate))
  (sp-local-pair "[[" "]]" :actions '(wrap insert navigate))
  (sp-local-pair "[[ " " ]]" :actions '(wrap insert navigate)))

The code to create a simple if..fi block would look something like this:

(defun sp-shell-post-handler (id action _context)
  (when (eq action 'insert)
    (insert " ")
    (save-excursion
      (indent-according-to-mode)
      (newline))))

(sp-local-pair 'sh-mode "if" "fi"
           :when '(("TAB"))
           :unless '(sp-in-string-p sp-in-comment-p)
           :actions '(insert navigate)
           :post-handlers '(:add sp-shell-post-handler))
4
  • I like this, but I just noticed that my init file's use-package invocation of smartparens has (progn (require 'smartparens-config) ; (smartparens-global-mode 1) )) (which in the comment will get confused, but the smartparens-global-mode line is commented out). Anywhere I run C-h k <ret> {,(,[,',", I get self-insert, so how can I determine if I'm actually using smartparens, or something else, in sh-script-mode?
    – Trey
    Commented Apr 2, 2019 at 17:41
  • If you want smartparens enabled only in shell you have to add smartparens-mode to sh-mode-hook. You should see SP in the modeline. I think the self-insert-command result you're seeing is expected, even with sp enabled.
    – jagrg
    Commented Apr 2, 2019 at 21:33
  • Sorry, I think I misstated my question... I'm not entirely sure the thing that's doubling my brackets and quotes is smartparens. I know from my config variables that in my C# mode configuration it's electric-pair-mode. In sh-script-mode, if I output the entire list of minor modes active, smartparens, electric-pair, and autopair are all listed. So how can I find out what's being called when I hit a particular key if its binding is just self-insert-command?
    – Trey
    Commented Apr 3, 2019 at 0:50
  • Hm. I'm not sure. I suppose you could isolate sp by disabling the other modes. This would tell you if sp is doing its job if that's what you want to know.
    – jagrg
    Commented Apr 3, 2019 at 9:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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