1

I have the following bash array after automatic indentation, both on pressing carriage return when was editing in :h Insert mode and doing =ip on the block in :h Normal-mode

DOTFILES=(
".bashrc"
".bash_profile"
".gitconfig"
".profile"
)

Trying with nvim -u NORC yielded the same result, so I'm guessing it's the default /usr/share/nvim/runtime/indent/sh.vim indent file in action. Output of :set ft? is filetype=sh.

I would like to have the aforementioned array with the following indentation:

DOTFILES=(
    ".bashrc"
    ".bash_profile"
    ".gitconfig"
    ".profile"
)

How do I do that?

  • set ft? returns filetype=sh. And let g:is_bash=1 doesn't have any effect. I entered let g:is_bash=1 from :h Command-line-mode – klaus Mar 17 at 5:10
  • @D.BenKnoble, I set it in my init.vim (using neovim). Also checked out the documentation. I think g:is_bash if for syntax highlighting only, not sure though. But it doesn't work for the multiline array indentation. – klaus Mar 17 at 20:42
  • 1
    I was wrong; I grep'd through my shell scripts to find these arrays in vim and tried running gqip—each one was re-indented to be like your first example. I think I manually do the first line, and subsequent lines keep the indentation (via what magic combination of indent settings, I'm not 100% sure). Good question; +1 from me. – D. Ben Knoble Mar 17 at 20:46
  • At a guess, one could extend the shell indentation in ~/.vim/after/indent/sh.vim to detect these arrays and do "the right thing," falling back on the original $VIMRUNTIME/indent/sh.vim values as necessary. – D. Ben Knoble Mar 17 at 20:49
  • @D.BenKnoble, Ok. I'll try to learn how to create indent files. I also have something with php+html indentation bugging me. Who knows, maybe I can fix that too. – klaus Mar 18 at 4:00
0

From https://github.com/chrisbra/vim-sh-indent

If you have vim with :h packages, you can do the following:

$ mkdir -p ~/.vim/pack/dist/start # if using neovim: $ mkdir -p ~/.local/share/nvim/site/pack/dist/start
$ cd ~/.vim/pack/dist/start       # if using neovim: $ cd ~/.local/share/nvim/site/pack/dist/start
$ git clone https://github.com/chrisbra/vim-sh-indent
$ cd -

If you don't have ':h packages` feature in your version of neo/vim, or you just don't want to use it, you can either use a plugin manager, or just put the following indent file in your user-specific indent directory.

Put the following in ~/.config/nvim/indent/sh.vim or ~/.vim/indent/sh.vim if you're using nvim or vim respectively:

" Vim indent file
" Language:            Shell Script
" Maintainer:          Christian Brabandt <cb@256bit.org>
" Original Author:     Nikolai Weibull <now@bitwi.se>
" Previous Maintainer: Peter Aronoff <telemachus@arpinum.org>
" Latest Revision:     2019-03-19
" License:             Vim (see :h license)
" Repository:          https://github.com/chrisbra/vim-sh-indent
" Changelog:
"          20190319  - Indent arrays (only zsh and bash)
"                      https://github.com/chrisbra/vim-sh-indent/issues/13
"          20190316  - Make use of searchpairpos for nested if sections
"                      fixes https://github.com/chrisbra/vim-sh-indent/issues/11
"          20190201  - Better check for closing if sections
"          20180724  - make check for zsh syntax more rigid (needs word-boundaries)
"          20180326  - better support for line continuation
"          20180325  - better detection of function definitions
"          20180127  - better support for zsh complex commands
"          20170808: - better indent of line continuation
"          20170502: - get rid of buffer-shiftwidth function
"          20160912: - preserve indentation of here-doc blocks
"          20160627: - detect heredocs correctly
"          20160213: - detect function definition correctly
"          20160202: - use shiftwidth() function
"          20151215: - set b:undo_indent variable
"          20150728: - add foreach detection for zsh

if exists("b:did_indent")
  finish
endif
let b:did_indent = 1

setlocal indentexpr=GetShIndent()
setlocal indentkeys+=0=then,0=do,0=else,0=elif,0=fi,0=esac,0=done,0=end,),0=;;,0=;&
setlocal indentkeys+=0=fin,0=fil,0=fip,0=fir,0=fix
setlocal indentkeys-=:,0#
setlocal nosmartindent

let b:undo_indent = 'setlocal indentexpr< indentkeys< smartindent<'

if exists("*GetShIndent")
  finish
endif

let s:cpo_save = &cpo
set cpo&vim

let s:sh_indent_defaults = {
      \ 'default': function('shiftwidth'),
      \ 'continuation-line': function('shiftwidth'),
      \ 'case-labels': function('shiftwidth'),
      \ 'case-statements': function('shiftwidth'),
      \ 'case-breaks': 0 }

function! s:indent_value(option)
  let Value = exists('b:sh_indent_options')
            \ && has_key(b:sh_indent_options, a:option) ?
            \ b:sh_indent_options[a:option] :
            \ s:sh_indent_defaults[a:option]
  if type(Value) == type(function('type'))
    return Value()
  endif
  return Value
endfunction

function! GetShIndent()
  let curline = getline(v:lnum)
  let lnum = prevnonblank(v:lnum - 1)
  if lnum == 0
    return 0
  endif
  let line = getline(lnum)

  let pnum = prevnonblank(lnum - 1)
  let pline = getline(pnum)
  let ind = indent(lnum)

  " Check contents of previous lines
  if line =~ '^\s*\%(if\|then\|do\|else\|elif\|case\|while\|until\|for\|select\|foreach\)\>' ||
        \  (&ft is# 'zsh' && line =~ '\<\%(if\|then\|do\|else\|elif\|case\|while\|until\|for\|select\|foreach\)\>')
    if !s:is_end_expression(line)
      let ind += s:indent_value('default')
    endif
  elseif s:is_case_label(line, pnum)
    if !s:is_case_ended(line)
      let ind += s:indent_value('case-statements')
    endif
  " function definition
  elseif s:is_function_definition(line)
    if line !~ '}\s*\%(#.*\)\=$'
      let ind += s:indent_value('default')
    endif
  " array (only works for zsh or bash)
  elseif s:is_array(line) && line !~ ')\s*$' && (&ft is# 'zsh' || s:is_bash())
      let ind += s:indent_value('continuation-line')
  " end of array
  elseif curline =~ '^\s*)$'
      let ind -= s:indent_value('continuation-line')
  elseif s:is_continuation_line(line)
    if pnum == 0 || !s:is_continuation_line(pline)
      let ind += s:indent_value('continuation-line')
    endif
  elseif s:end_block(line) && !s:start_block(line)
    let ind -= s:indent_value('default')
  elseif pnum != 0 &&
        \ s:is_continuation_line(pline) &&
        \ !s:end_block(curline) &&
        \ !s:is_end_expression(curline)
    " only add indent, if line and pline is in the same block
    let i = v:lnum
    let ind2 = indent(s:find_continued_lnum(pnum))
    while !s:is_empty(getline(i)) && i > pnum
      let i -= 1
    endw
    if i == pnum
      let ind += ind2
    else
      let ind = ind2
    endif
  endif

  let pine = line
  " Check content of current line
  let line = curline
  " Current line is a endif line, so get indent from start of "if condition" line
  " TODO: should we do the same for other "end" lines?
  if curline =~ '^\s*\%(fi\)\s*\%(#.*\)\=$'
    let previous_line = searchpair('\<if\>', '', '\<fi\>', 'bnW')
    if previous_line > 0
      let ind = indent(previous_line)
    endif
  elseif line =~ '^\s*\%(then\|do\|else\|elif\|done\|end\)\>' || s:end_block(line)
    let ind -= s:indent_value('default')
  elseif line =~ '^\s*esac\>' && s:is_case_empty(getline(v:lnum - 1))
    let ind -= s:indent_value('default')
  elseif line =~ '^\s*esac\>'
    let ind -= (s:is_case_label(pine, lnum) && s:is_case_ended(pine) ?
             \ 0 : s:indent_value('case-statements')) +
             \ s:indent_value('case-labels')
    if s:is_case_break(pine)
      let ind += s:indent_value('case-breaks')
    endif
  elseif s:is_case_label(line, lnum)
    if s:is_case(pine)
      let ind = indent(lnum) + s:indent_value('case-labels')
    else
      let ind -= (s:is_case_label(pine, lnum) && s:is_case_ended(pine) ?
                  \ 0 : s:indent_value('case-statements')) -
                  \ s:indent_value('case-breaks')
    endif
  elseif s:is_case_break(line)
    let ind -= s:indent_value('case-breaks')
  elseif s:is_here_doc(line)
    let ind = 0
  " statements, executed within a here document. Keep the current indent
  elseif match(map(synstack(v:lnum, 1), 'synIDattr(v:val, "name")'), '\c\mheredoc') > -1
    return indent(v:lnum)
  elseif s:is_comment(line) && s:is_empty(getline(v:lnum-1))
    return indent(v:lnum)
  endif

  return ind > 0 ? ind : 0
endfunction

function! s:is_continuation_line(line)
  " Comment, cannot be a line continuation
  if a:line =~ '^\s*#'
    return 0
  else
    " start-of-line
    " \\ or && or || or |
    " followed optionally by { or #
    return a:line =~ '\%(\%(^\|[^\\]\)\\\|&&\|||\||\)' .
                 \ '\s*\({\s*\)\=\(#.*\)\=$'
  endif
endfunction

function! s:find_continued_lnum(lnum)
  let i = a:lnum
  while i > 1 && s:is_continuation_line(getline(i - 1))
    let i -= 1
  endwhile
  return i
endfunction

function! s:is_function_definition(line)
  return a:line =~ '^\s*\<\k\+\>\s*()\s*{' ||
       \ a:line =~ '^\s*{' ||
       \ a:line =~ '^\s*function\s*\w\S\+\s*\%(()\)\?\s*{'
endfunction

function! s:is_array(line)
  return a:line =~ '^\s*\<\k\+\>=('
endfunction

function! s:is_case_label(line, pnum)
  if a:line !~ '^\s*(\=.*)'
    return 0
  endif

  if a:pnum > 0
    let pine = getline(a:pnum)
    if !(s:is_case(pine) || s:is_case_ended(pine))
      return 0
    endif
  endif

  let suffix = substitute(a:line, '^\s*(\=', "", "")
  let nesting = 0
  let i = 0
  let n = strlen(suffix)
  while i < n
    let c = suffix[i]
    let i += 1
    if c == '\\'
      let i += 1
    elseif c == '('
      let nesting += 1
    elseif c == ')'
      if nesting == 0
        return 1
      endif
      let nesting -= 1
    endif
  endwhile
  return 0
endfunction

function! s:is_case(line)
  return a:line =~ '^\s*case\>'
endfunction

function! s:is_case_break(line)
  return a:line =~ '^\s*;[;&]'
endfunction

function! s:is_here_doc(line)
    if a:line =~ '^\w\+$'
      let here_pat = '<<-\?'. s:escape(a:line). '\$'
      return search(here_pat, 'bnW') > 0
    endif
    return 0
endfunction

function! s:is_case_ended(line)
  return s:is_case_break(a:line) || a:line =~ ';[;&]\s*\%(#.*\)\=$'
endfunction

function! s:is_case_empty(line)
  if a:line =~ '^\s*$' || a:line =~ '^\s*#'
    return s:is_case_empty(getline(v:lnum - 1))
  else
    return a:line =~ '^\s*case\>'
  endif
endfunction

function! s:escape(pattern)
    return '\V'. escape(a:pattern, '\\')
endfunction

function! s:is_empty(line)
  return a:line =~ '^\s*$'
endfunction

function! s:end_block(line)
  return a:line =~ '^\s*}'
endfunction

function! s:start_block(line)
  return a:line =~ '{\s*\(#.*\)\?$'
endfunction

function! s:find_start_block(lnum)
  let i = a:lnum
  while i > 1 && !s:start_block(getline(i))
    let i -= 1
  endwhile
  return i
endfunction

function! s:is_comment(line)
  return a:line =~ '^\s*#'
endfunction

function! s:is_end_expression(line)
  return a:line =~ '\<\%(fi\|esac\|done\|end\)\>\s*\%(#.*\)\=$'
endfunction

function! s:is_bash()
  return get(g:, 'is_bash', 0) || get(b:, 'is_bash', 0)
endfunction

let &cpo = s:cpo_save
unlet s:cpo_save
  • it would be preferable to simply clone it below pack/dist/start and then it should simply work. Or wait until it is included with Vim with the next runtime file update – Christian Brabandt Mar 19 at 13:16
  • Added information about :h packages. Thanks. – klaus Mar 20 at 14:41

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.