sh.vim (10876B)
1 " Vim indent file 2 " Language: Shell Script 3 " Maintainer: Christian Brabandt <cb@256bit.org> 4 " Original Author: Nikolai Weibull <now@bitwi.se> 5 " Previous Maintainer: Peter Aronoff <telemachus@arpinum.org> 6 " Latest Revision: 2019-10-24 7 " License: Vim (see :h license) 8 " Repository: https://github.com/chrisbra/vim-sh-indent 9 " Changelog: 10 " 20250906 - indent function closing properly on multiline commands 11 " 20250318 - Detect local arrays in functions 12 " 20241411 - Detect dash character in function keyword for 13 " bash mode (issue #16049) 14 " 20190726 - Correctly skip if keywords in syntax comments 15 " (issue #17) 16 " 20190603 - Do not indent in zsh filetypes with an `if` in comments 17 " 20190428 - De-indent fi correctly when typing with 18 " https://github.com/chrisbra/vim-sh-indent/issues/15 19 " 20190325 - Indent fi; correctly 20 " https://github.com/chrisbra/vim-sh-indent/issues/14 21 " 20190319 - Indent arrays (only zsh and bash) 22 " https://github.com/chrisbra/vim-sh-indent/issues/13 23 " 20190316 - Make use of searchpairpos for nested if sections 24 " fixes https://github.com/chrisbra/vim-sh-indent/issues/11 25 " 20190201 - Better check for closing if sections 26 " 20180724 - make check for zsh syntax more rigid (needs word-boundaries) 27 " 20180326 - better support for line continuation 28 " 20180325 - better detection of function definitions 29 " 20180127 - better support for zsh complex commands 30 " 20170808: - better indent of line continuation 31 " 20170502: - get rid of buffer-shiftwidth function 32 " 20160912: - preserve indentation of here-doc blocks 33 " 20160627: - detect heredocs correctly 34 " 20160213: - detect function definition correctly 35 " 20160202: - use shiftwidth() function 36 " 20151215: - set b:undo_indent variable 37 " 20150728: - add foreach detection for zsh 38 39 if exists("b:did_indent") 40 finish 41 endif 42 let b:did_indent = 1 43 44 setlocal indentexpr=GetShIndent() 45 setlocal indentkeys+=0=then,0=do,0=else,0=elif,0=fi,0=esac,0=done,0=end,),0=;;,0=;& 46 setlocal indentkeys+=0=fin,0=fil,0=fip,0=fir,0=fix 47 setlocal indentkeys-=:,0# 48 setlocal nosmartindent 49 50 let b:undo_indent = 'setlocal indentexpr< indentkeys< smartindent<' 51 52 if exists("*GetShIndent") 53 finish 54 endif 55 56 let s:cpo_save = &cpo 57 set cpo&vim 58 59 let s:sh_indent_defaults = { 60 \ 'default': function('shiftwidth'), 61 \ 'continuation-line': function('shiftwidth'), 62 \ 'case-labels': function('shiftwidth'), 63 \ 'case-statements': function('shiftwidth'), 64 \ 'case-breaks': 0 } 65 66 function! s:indent_value(option) 67 let Value = exists('b:sh_indent_options') 68 \ && has_key(b:sh_indent_options, a:option) ? 69 \ b:sh_indent_options[a:option] : 70 \ s:sh_indent_defaults[a:option] 71 if type(Value) == type(function('type')) 72 return Value() 73 endif 74 return Value 75 endfunction 76 77 function! GetShIndent() 78 let mode = mode() 79 80 let curline = getline(v:lnum) 81 let lnum = prevnonblank(v:lnum - 1) 82 if lnum == 0 83 return 0 84 endif 85 let line = getline(lnum) 86 87 let pnum = prevnonblank(lnum - 1) 88 let pline = getline(pnum) 89 let ind = indent(lnum) 90 91 " Check contents of previous lines 92 " should not apply to e.g. commented lines 93 94 if s:start_block(line) 95 let ind += s:indent_value('default') 96 elseif line =~ '^\s*\%(if\|then\|do\|else\|elif\|case\|while\|until\|for\|select\|foreach\)\>\($\|\s\)' || 97 \ (&ft is# 'zsh' && line =~ '^\s*\<\%(if\|then\|do\|else\|elif\|case\|while\|until\|for\|select\|foreach\)\>\($\|\s\)') 98 if !s:is_end_expression(line) 99 let ind += s:indent_value('default') 100 endif 101 elseif s:is_case_label(line, pnum) 102 if !s:is_case_ended(line) 103 let ind += s:indent_value('case-statements') 104 endif 105 " function definition 106 elseif s:is_function_definition(line) 107 if line !~ '}\s*\%(#.*\)\=$' 108 let ind += s:indent_value('default') 109 endif 110 " array (only works for zsh or bash) 111 elseif s:is_array(line) && line !~ ')\s*$' && (&ft is# 'zsh' || s:is_bash()) 112 let ind += s:indent_value('continuation-line') 113 " end of array 114 elseif curline =~ '^\s*)$' 115 let ind -= s:indent_value('continuation-line') 116 elseif s:is_continuation_line(line) 117 if pnum == 0 || !s:is_continuation_line(pline) 118 let ind += s:indent_value('continuation-line') 119 endif 120 elseif s:end_block(line) && !s:start_block(line) 121 let ind -= s:indent_value('default') 122 elseif pnum != 0 && 123 \ s:is_continuation_line(pline) && 124 \ !s:end_block(curline) && 125 \ !s:is_end_expression(curline) 126 " only add indent, if line and pline is in the same block 127 let i = v:lnum 128 let ind2 = indent(s:find_continued_lnum(pnum)) 129 while !s:is_empty(getline(i)) && i > pnum 130 let i -= 1 131 endw 132 if i == pnum && (s:is_continuation_line(line) || pline =~ '{\s*\(#.*\)\=$') 133 let ind += ind2 134 else 135 let ind = ind2 136 endif 137 endif 138 139 let pine = line 140 " Check content of current line 141 let line = curline 142 " Current line is a endif line, so get indent from start of "if condition" line 143 " TODO: should we do the same for other "end" lines? 144 if curline =~ '^\s*\%(fi\);\?\s*\%(#.*\)\=$' 145 let ind = indent(v:lnum) 146 " in insert mode, try to place the cursor after the fi statement 147 let endp = '\<fi\>' .. (mode ==? 'i' ? '\zs' : '') 148 let startp = '^\s*\<if\>' 149 let previous_line = searchpair(startp, '', endp , 'bnW', 150 \ 'synIDattr(synID(line("."),col("."), 1),"name") =~? "comment\\|quote\\|option"') 151 if previous_line > 0 152 let ind = indent(previous_line) 153 endif 154 elseif line =~ '^\s*\%(then\|do\|else\|elif\|done\|end\)\>' || s:end_block(line) 155 let ind -= s:indent_value('default') 156 elseif line =~ '^\s*esac\>' && s:is_case_empty(getline(v:lnum - 1)) 157 let ind -= s:indent_value('default') 158 elseif line =~ '^\s*esac\>' 159 let ind -= (s:is_case_label(pine, lnum) && s:is_case_ended(pine) ? 160 \ 0 : s:indent_value('case-statements')) + 161 \ s:indent_value('case-labels') 162 if s:is_case_break(pine) 163 let ind += s:indent_value('case-breaks') 164 endif 165 elseif s:is_case_label(line, lnum) 166 if s:is_case(pine) 167 let ind = indent(lnum) + s:indent_value('case-labels') 168 else 169 let ind -= (s:is_case_label(pine, lnum) && s:is_case_ended(pine) ? 170 \ 0 : s:indent_value('case-statements')) - 171 \ s:indent_value('case-breaks') 172 endif 173 elseif s:is_case_break(line) 174 let ind -= s:indent_value('case-breaks') 175 elseif s:is_here_doc(line) 176 let ind = 0 177 " statements, executed within a here document. Keep the current indent 178 elseif match(map(synstack(v:lnum, 1), 'synIDattr(v:val, "name")'), '\c\mheredoc') > -1 179 return indent(v:lnum) 180 elseif s:is_comment(line) && s:is_empty(getline(v:lnum-1)) 181 if s:is_in_block(v:lnum) 182 " return indent of line in same block 183 return indent(lnum) 184 else 185 " use indent of current line 186 return indent(v:lnum) 187 endif 188 endif 189 190 " Special case: if the current line is a closing '}', align with matching '{' 191 if curline =~ '^\s*}\s*$' 192 let match_lnum = searchpair('{', '', '}', 'bnW', 193 \ 'synIDattr(synID(line("."),col("."), 1),"name") =~? "comment\\|quote"') 194 if match_lnum > 0 195 return indent(match_lnum) 196 endif 197 endif 198 199 return ind > 0 ? ind : 0 200 endfunction 201 202 function! s:is_continuation_line(line) 203 " Comment, cannot be a line continuation 204 if a:line =~ '^\s*#' 205 return 0 206 else 207 " start-of-line 208 " \\ or && or || or | 209 " followed optionally by { or # 210 return a:line =~ '\%(\%(^\|[^\\]\)\\\|&&\|||\||\)' . 211 \ '\s*\({\s*\)\=\(#.*\)\=$' 212 endif 213 endfunction 214 215 function! s:find_continued_lnum(lnum) 216 let i = a:lnum 217 while i > 1 && s:is_continuation_line(getline(i - 1)) 218 let i -= 1 219 endwhile 220 return i 221 endfunction 222 223 function! s:is_function_definition(line) 224 return a:line =~ '^\s*\<\k\+\>\s*()\s*{' || 225 \ a:line =~ '^\s*{' || 226 \ a:line =~ '^\s*function\s*\k\+\s*\%(()\)\?\s*{' || 227 \ ((&ft is# 'zsh' || s:is_bash()) && 228 \ a:line =~ '^\s*function\s*\S\+\s*\%(()\)\?\s*{' ) 229 endfunction 230 231 function! s:is_array(line) 232 return a:line =~ '^\s*\(\(declare\|typeset\|local\)\s\+\(-[Aalrtu]\+\s\+\)\?\)\?\<\k\+\>=(' 233 endfunction 234 235 function! s:is_in_block(line) 236 " checks whether a:line is whithin a 237 " block e.g. a shell function 238 " foo() { 239 " .. 240 " } 241 let prevline = searchpair('{', '', '}', 'bnW', 'synIDattr(synID(line("."),col("."), 1),"name") =~? "comment\\|quote"') 242 let nextline = searchpair('{', '', '}', 'nW', 'synIDattr(synID(line("."),col("."), 1),"name") =~? "comment\\|quote"') 243 return a:line > prevline && a:line < nextline 244 endfunction 245 246 function! s:is_case_label(line, pnum) 247 if a:line !~ '^\s*(\=.*)' 248 return 0 249 endif 250 251 if a:pnum > 0 252 let pine = getline(a:pnum) 253 if !(s:is_case(pine) || s:is_case_ended(pine)) 254 return 0 255 endif 256 endif 257 258 let suffix = substitute(a:line, '^\s*(\=', "", "") 259 let nesting = 0 260 let i = 0 261 let n = strlen(suffix) 262 while i < n 263 let c = suffix[i] 264 let i += 1 265 if c == '\\' 266 let i += 1 267 elseif c == '(' 268 let nesting += 1 269 elseif c == ')' 270 if nesting == 0 271 return 1 272 endif 273 let nesting -= 1 274 endif 275 endwhile 276 return 0 277 endfunction 278 279 function! s:is_case(line) 280 return a:line =~ '^\s*case\>' 281 endfunction 282 283 function! s:is_case_break(line) 284 return a:line =~ '^\s*;[;&]' 285 endfunction 286 287 function! s:is_here_doc(line) 288 if a:line =~ '^\w\+$' 289 let here_pat = '<<-\?'. s:escape(a:line). '\$' 290 return search(here_pat, 'bnW') > 0 291 endif 292 return 0 293 endfunction 294 295 function! s:is_case_ended(line) 296 return s:is_case_break(a:line) || a:line =~ ';[;&]\s*\%(#.*\)\=$' 297 endfunction 298 299 function! s:is_case_empty(line) 300 if a:line =~ '^\s*$' || a:line =~ '^\s*#' 301 return s:is_case_empty(getline(v:lnum - 1)) 302 else 303 return a:line =~ '^\s*case\>' 304 endif 305 endfunction 306 307 function! s:escape(pattern) 308 return '\V'. escape(a:pattern, '\\') 309 endfunction 310 311 function! s:is_empty(line) 312 return a:line =~ '^\s*$' 313 endfunction 314 315 function! s:end_block(line) 316 return a:line =~ '^\s*}' 317 endfunction 318 319 function! s:start_block(line) 320 return a:line =~ '^[^#]*[{(]\s*\(#.*\)\?$' 321 endfunction 322 323 function! s:is_comment(line) 324 return a:line =~ '^\s*#' 325 endfunction 326 327 function! s:is_end_expression(line) 328 return a:line =~ '\<\%(fi\|esac\|done\|end\)\>\s*\%(#.*\)\=$' 329 endfunction 330 331 function! s:is_bash() 332 if &ft is# 'bash' || getline(1) is# '#!/bin/bash' 333 return v:true 334 else 335 return get(g:, 'is_bash', 0) || get(b:, 'is_bash', 0) 336 endif 337 endfunction 338 339 let &cpo = s:cpo_save 340 unlet s:cpo_save