6

The problem

In shell scripting, conditionals and loops don't use curly braces to indicate blocks, but rather special keywords: if [ ... ]; then statements close with fi, while [ ... ]; do statements close with done, and case statements close with esac.

Without mappings, the normal protocol to complete, e.g., a then...fi pair would be the following sequence of keys, starting in insert mode: then<CR><BS>fi<Esc>O. I'm not very fond of this workflow since it takes so many keystrokes and exits insert mode, so I've come up with my own solution. I'm not sure if this is the best solution though, so I'd appreciate recommendations if you have any.

My current solution

I use the following mappings to facilitate easier completion of pairs:

" if statements:
inoremap fi<CR> <BS>fi<Esc>O
" case statements:
inoremap c<CR> c<Esc>O
" for, while, until, and select loops:
inoremap one<CR> one<Esc>O

This leads to the following workflow when creating then...fi, do...done, and case...esac pairs:

then...fi pairs:   then<CR>fi<CR>
do...done pairs:   do<CR>done<CR>
case...esac pairs: case[...]<CR>esac<CR>

Things I want the mapping to do

I want a mapping to:

  1. Allow me to complete pairs without manually exiting insert mode.
  2. End with my cursor between the two lines and indented one tab, like so (cursor indicated with [ ]):

    if [ ... ]
    then
            []
    fi
    
  3. Be a similar calling workflow for all 3 pair types.
  4. Require the least amount of keystrokes while avoiding things I don't want, as described below.

Things I want the mapping to NOT do

I want to avoid the following problems:

  1. Trigger in situations when I wouldn't want it to. For example, I wouldn't want the mapping inoremap do<CR> do<CR>done<Esc>O since that would also trigger when I'm writing do...while loops in C++. If this can be worked around to only trigger in files with certain file extensions, that would be an exception.
  2. Be distracting while writing code normally. For example, I mapped one<CR> instead of done<CR> since I want to be able to see the letters as I type them unless I'm probably trying to use the mapping (i.e. done<CR> would hide my characters while typing do until I typed a character other than n, which I don't like).

Do you have recommendations for changes? Or do you think my mappings best fit what I want and don't want?

  • 1
    Use a snippet plugin might be a better choice. – dedowsdi Apr 15 at 8:46
3

Here's what I would go for:

autocmd FileType sh iab <buffer> then then<CR>fi<C-o>O<SPACE><BS><C-o>z

That's for the then...fi, but you can apply it for the 2 other solutions as well

Explanation

  • autocmd allows you to specify a filetype (among other things), here sh
  • I use abbreviations (iab) instead of mapping, as this doesn't hide the characters as you type them. The downside though is that the mapping won't be triggered by <CR>, but by <SPACE>, or <ESC>
  • <buffer> limits this abbreviation to the current buffer

And about the mapping:

  • then is what you need to type in order to trigger the abbreviation
  • then (again) is there because we actually want to write "then"
  • <CR> We enter a newline
  • fi The characters "fi"
  • <C-o>O We create a newline above
  • <SPACE><BS> This is just here to avoid the indent to collapse when we enter normal mode
  • <C-o>z is here to cancel the <SPACE> that's about to be entered (iab always enters the triggering character at the end of the abbreviation)


If you want to be fancy, you can even write a little function:

function! CloseSh(start, end, before)
    " Entering the actual start
    exe"norm!i".a:start

    " Making sure that the 'before' is on the same line,
    " or one line before cursor
    if search('.*'.a:before.'.*\n\?.*\%#', 'n') == '0'
        " If it's not the case, we behave as if nothing happened
        exe"norm!a\<SPACE>"
        call setpos('.', [0, line('.'), 1000, 0, 1000])
        return
    endif

    " If match found, we can continue:
    " adding the end tag bellow
    exe"norm!o".a:end
    " new line above"
    exe"norm!O\<SPACE>\<BS>"
    " force cursor at the end of line
    call setpos('.', [0, line('.'), 1000, 0, 1000])
endfunction

autocmd FileType sh inoreab <buffer> then :call CloseSh('then', 'fi', '\<if\>')<CR><C-o>z
autocmd FileType sh inoreab <buffer> in :call CloseSh('in', 'esac', '\<case\>')<CR><C-o>z
autocmd FileType sh inoreab <buffer> do :call CloseSh('do', 'done', '\<while\>')<CR><C-o>z

This function will make sure that the tag you're typing is preceded by the right thing (e.g. "then" is preceded by "if") so that it doesn't trigger when writing comments for example


Also, as @filbranden said:

❝ instead of creating autocmd FileType sh you could simply store these iabs in a file after/ftplugin/sh.vim in your ~/.vim/

| improve this answer | |
  • 1
    I like it! You should note that <cmd> is specific to NeoVim. Also instead of creating autocmd FileType sh you could simply store these iabs in a file after/ftplugin/sh.vim in your ~/.vim (or NeoVim equivalent.) – filbranden Apr 15 at 11:58
  • Upon actually testing the recommendations here (2 months after accepting...), the function does not work. The following keystrokes if true<CR>then<Space> produce if true<CR><Tab>:call CloseSh('then', 'fi', '\<if\>')<CR>. Swapping <Space> with <CR> in the keystrokes produces the same problem but also moves the cursor to the top of the screen like zt would. – Drake P Jul 14 at 1:58
3

Personally, I’ve used endwise for a few years and been satisfied. It’s easy enough to add other languages if necessary, but it supports shell (among many others) out of the box.

It will automatically insert « ending » statements when you hit enter, so if ... <CR> and the fi is added.

Perhaps my favorite part is endwise is smart enough not to add the end if one is already there.

| improve this answer | |

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.