blob: a1abd1d0afa85238a518d1d292ec704b41087f40 [file] [log] [blame]
Bram Moolenaar071d4272004-06-13 20:20:40 +00001" Vim indent file
Bram Moolenaarfc39ecf2015-08-11 20:34:49 +02002" Language: Shell Script
3" Maintainer: Christian Brabandt <cb@256bit.org>
Bram Moolenaarfc39ecf2015-08-11 20:34:49 +02004" Original Author: Nikolai Weibull <now@bitwi.se>
Bram Moolenaareb3dc872018-05-13 22:34:24 +02005" Previous Maintainer: Peter Aronoff <telemachus@arpinum.org>
Bram Moolenaar96f45c02019-10-26 19:53:45 +02006" Latest Revision: 2019-10-24
Bram Moolenaarfc39ecf2015-08-11 20:34:49 +02007" License: Vim (see :h license)
8" Repository: https://github.com/chrisbra/vim-sh-indent
Bram Moolenaare18dbe82016-07-02 21:42:23 +02009" Changelog:
Christian Brabandt711f4a02025-03-18 22:36:28 +010010" 20250318 - Detect local arrays in functions
Lukas Zapletal0acd3ab2024-11-14 21:50:15 +010011" 20241411 - Detect dash character in function keyword for
12" bash mode (issue #16049)
Bram Moolenaar54775062019-07-31 21:07:14 +020013" 20190726 - Correctly skip if keywords in syntax comments
14" (issue #17)
15" 20190603 - Do not indent in zsh filetypes with an `if` in comments
Bram Moolenaara6c27c42019-05-09 19:16:22 +020016" 20190428 - De-indent fi correctly when typing with
17" https://github.com/chrisbra/vim-sh-indent/issues/15
Bram Moolenaar62e1bb42019-04-08 16:25:07 +020018" 20190325 - Indent fi; correctly
19" https://github.com/chrisbra/vim-sh-indent/issues/14
20" 20190319 - Indent arrays (only zsh and bash)
21" https://github.com/chrisbra/vim-sh-indent/issues/13
22" 20190316 - Make use of searchpairpos for nested if sections
23" fixes https://github.com/chrisbra/vim-sh-indent/issues/11
24" 20190201 - Better check for closing if sections
Bram Moolenaar91f84f62018-07-29 15:07:52 +020025" 20180724 - make check for zsh syntax more rigid (needs word-boundaries)
Bram Moolenaareb3dc872018-05-13 22:34:24 +020026" 20180326 - better support for line continuation
27" 20180325 - better detection of function definitions
28" 20180127 - better support for zsh complex commands
Bram Moolenaar1ccd8ff2017-08-11 19:50:37 +020029" 20170808: - better indent of line continuation
Bram Moolenaarb4d6c3e2017-05-27 16:45:17 +020030" 20170502: - get rid of buffer-shiftwidth function
31" 20160912: - preserve indentation of here-doc blocks
Bram Moolenaare18dbe82016-07-02 21:42:23 +020032" 20160627: - detect heredocs correctly
33" 20160213: - detect function definition correctly
34" 20160202: - use shiftwidth() function
35" 20151215: - set b:undo_indent variable
36" 20150728: - add foreach detection for zsh
Bram Moolenaar071d4272004-06-13 20:20:40 +000037
Bram Moolenaar071d4272004-06-13 20:20:40 +000038if exists("b:did_indent")
39 finish
40endif
Bram Moolenaar071d4272004-06-13 20:20:40 +000041let b:did_indent = 1
42
43setlocal indentexpr=GetShIndent()
Bram Moolenaarfc39ecf2015-08-11 20:34:49 +020044setlocal indentkeys+=0=then,0=do,0=else,0=elif,0=fi,0=esac,0=done,0=end,),0=;;,0=;&
Bram Moolenaar5c736222010-01-06 20:54:52 +010045setlocal indentkeys+=0=fin,0=fil,0=fip,0=fir,0=fix
Bram Moolenaar071d4272004-06-13 20:20:40 +000046setlocal indentkeys-=:,0#
Bram Moolenaar5c736222010-01-06 20:54:52 +010047setlocal nosmartindent
Bram Moolenaar071d4272004-06-13 20:20:40 +000048
Bram Moolenaarf3913272016-02-25 00:00:01 +010049let b:undo_indent = 'setlocal indentexpr< indentkeys< smartindent<'
50
Bram Moolenaar071d4272004-06-13 20:20:40 +000051if exists("*GetShIndent")
52 finish
53endif
54
Bram Moolenaar42eeac32005-06-29 22:40:58 +000055let s:cpo_save = &cpo
56set cpo&vim
Bram Moolenaar071d4272004-06-13 20:20:40 +000057
Bram Moolenaar5c736222010-01-06 20:54:52 +010058let s:sh_indent_defaults = {
Bram Moolenaarb4d6c3e2017-05-27 16:45:17 +020059 \ 'default': function('shiftwidth'),
60 \ 'continuation-line': function('shiftwidth'),
61 \ 'case-labels': function('shiftwidth'),
62 \ 'case-statements': function('shiftwidth'),
Bram Moolenaar5c736222010-01-06 20:54:52 +010063 \ 'case-breaks': 0 }
64
65function! s:indent_value(option)
66 let Value = exists('b:sh_indent_options')
67 \ && has_key(b:sh_indent_options, a:option) ?
68 \ b:sh_indent_options[a:option] :
69 \ s:sh_indent_defaults[a:option]
70 if type(Value) == type(function('type'))
71 return Value()
72 endif
73 return Value
74endfunction
75
76function! GetShIndent()
Christian Brabandt711f4a02025-03-18 22:36:28 +010077 let mode = mode()
78
Bram Moolenaar62e1bb42019-04-08 16:25:07 +020079 let curline = getline(v:lnum)
Bram Moolenaar071d4272004-06-13 20:20:40 +000080 let lnum = prevnonblank(v:lnum - 1)
Bram Moolenaar071d4272004-06-13 20:20:40 +000081 if lnum == 0
82 return 0
83 endif
Bram Moolenaar7db25fe2018-05-13 00:02:36 +020084 let line = getline(lnum)
Bram Moolenaar071d4272004-06-13 20:20:40 +000085
Bram Moolenaar5c736222010-01-06 20:54:52 +010086 let pnum = prevnonblank(lnum - 1)
Bram Moolenaar7db25fe2018-05-13 00:02:36 +020087 let pline = getline(pnum)
Bram Moolenaar071d4272004-06-13 20:20:40 +000088 let ind = indent(lnum)
Bram Moolenaar7db25fe2018-05-13 00:02:36 +020089
90 " Check contents of previous lines
Bram Moolenaar54775062019-07-31 21:07:14 +020091 " should not apply to e.g. commented lines
Christian Brabandt711f4a02025-03-18 22:36:28 +010092
93 if s:start_block(line)
94 let ind += s:indent_value('default')
95 elseif line =~ '^\s*\%(if\|then\|do\|else\|elif\|case\|while\|until\|for\|select\|foreach\)\>\($\|\s\)' ||
96 \ (&ft is# 'zsh' && line =~ '^\s*\<\%(if\|then\|do\|else\|elif\|case\|while\|until\|for\|select\|foreach\)\>\($\|\s\)')
Bram Moolenaar62e1bb42019-04-08 16:25:07 +020097 if !s:is_end_expression(line)
Bram Moolenaar5c736222010-01-06 20:54:52 +010098 let ind += s:indent_value('default')
Bram Moolenaar071d4272004-06-13 20:20:40 +000099 endif
Bram Moolenaar5c736222010-01-06 20:54:52 +0100100 elseif s:is_case_label(line, pnum)
101 if !s:is_case_ended(line)
102 let ind += s:indent_value('case-statements')
103 endif
Bram Moolenaar7db25fe2018-05-13 00:02:36 +0200104 " function definition
105 elseif s:is_function_definition(line)
Bram Moolenaar5c736222010-01-06 20:54:52 +0100106 if line !~ '}\s*\%(#.*\)\=$'
107 let ind += s:indent_value('default')
108 endif
Bram Moolenaar62e1bb42019-04-08 16:25:07 +0200109 " array (only works for zsh or bash)
110 elseif s:is_array(line) && line !~ ')\s*$' && (&ft is# 'zsh' || s:is_bash())
111 let ind += s:indent_value('continuation-line')
112 " end of array
113 elseif curline =~ '^\s*)$'
114 let ind -= s:indent_value('continuation-line')
Bram Moolenaar5c736222010-01-06 20:54:52 +0100115 elseif s:is_continuation_line(line)
Bram Moolenaar7db25fe2018-05-13 00:02:36 +0200116 if pnum == 0 || !s:is_continuation_line(pline)
Bram Moolenaar5c736222010-01-06 20:54:52 +0100117 let ind += s:indent_value('continuation-line')
118 endif
Bram Moolenaar7db25fe2018-05-13 00:02:36 +0200119 elseif s:end_block(line) && !s:start_block(line)
Christian Brabandt711f4a02025-03-18 22:36:28 +0100120 let ind -= s:indent_value('default')
Bram Moolenaar62e1bb42019-04-08 16:25:07 +0200121 elseif pnum != 0 &&
122 \ s:is_continuation_line(pline) &&
123 \ !s:end_block(curline) &&
124 \ !s:is_end_expression(curline)
Bram Moolenaar7db25fe2018-05-13 00:02:36 +0200125 " only add indent, if line and pline is in the same block
126 let i = v:lnum
127 let ind2 = indent(s:find_continued_lnum(pnum))
128 while !s:is_empty(getline(i)) && i > pnum
129 let i -= 1
130 endw
Christian Brabandt711f4a02025-03-18 22:36:28 +0100131 if i == pnum && (s:is_continuation_line(line) || pline =~ '{\s*\(#.*\)\=$')
Bram Moolenaar7db25fe2018-05-13 00:02:36 +0200132 let ind += ind2
133 else
134 let ind = ind2
135 endif
Bram Moolenaar071d4272004-06-13 20:20:40 +0000136 endif
137
Bram Moolenaar5c736222010-01-06 20:54:52 +0100138 let pine = line
Bram Moolenaar7db25fe2018-05-13 00:02:36 +0200139 " Check content of current line
Bram Moolenaar62e1bb42019-04-08 16:25:07 +0200140 let line = curline
141 " Current line is a endif line, so get indent from start of "if condition" line
142 " TODO: should we do the same for other "end" lines?
143 if curline =~ '^\s*\%(fi\);\?\s*\%(#.*\)\=$'
Bram Moolenaar54775062019-07-31 21:07:14 +0200144 let ind = indent(v:lnum)
Christian Brabandt711f4a02025-03-18 22:36:28 +0100145 " in insert mode, try to place the cursor after the fi statement
146 let endp = '\<fi\>' .. (mode ==? 'i' ? '\zs' : '')
147 let startp = '^\s*\<if\>'
148 let previous_line = searchpair(startp, '', endp , 'bnW',
149 \ 'synIDattr(synID(line("."),col("."), 1),"name") =~? "comment\\|quote\\|option"')
Bram Moolenaar62e1bb42019-04-08 16:25:07 +0200150 if previous_line > 0
151 let ind = indent(previous_line)
152 endif
153 elseif line =~ '^\s*\%(then\|do\|else\|elif\|done\|end\)\>' || s:end_block(line)
Bram Moolenaar5c736222010-01-06 20:54:52 +0100154 let ind -= s:indent_value('default')
Bram Moolenaardfb18412013-12-11 18:53:29 +0100155 elseif line =~ '^\s*esac\>' && s:is_case_empty(getline(v:lnum - 1))
156 let ind -= s:indent_value('default')
Bram Moolenaar5c736222010-01-06 20:54:52 +0100157 elseif line =~ '^\s*esac\>'
158 let ind -= (s:is_case_label(pine, lnum) && s:is_case_ended(pine) ?
159 \ 0 : s:indent_value('case-statements')) +
160 \ s:indent_value('case-labels')
161 if s:is_case_break(pine)
162 let ind += s:indent_value('case-breaks')
163 endif
164 elseif s:is_case_label(line, lnum)
165 if s:is_case(pine)
166 let ind = indent(lnum) + s:indent_value('case-labels')
167 else
Bram Moolenaarfb539272014-08-22 19:21:47 +0200168 let ind -= (s:is_case_label(pine, lnum) && s:is_case_ended(pine) ?
169 \ 0 : s:indent_value('case-statements')) -
170 \ s:indent_value('case-breaks')
Bram Moolenaar5c736222010-01-06 20:54:52 +0100171 endif
172 elseif s:is_case_break(line)
173 let ind -= s:indent_value('case-breaks')
Bram Moolenaare18dbe82016-07-02 21:42:23 +0200174 elseif s:is_here_doc(line)
175 let ind = 0
Bram Moolenaarb4d6c3e2017-05-27 16:45:17 +0200176 " statements, executed within a here document. Keep the current indent
177 elseif match(map(synstack(v:lnum, 1), 'synIDattr(v:val, "name")'), '\c\mheredoc') > -1
178 return indent(v:lnum)
Bram Moolenaar7db25fe2018-05-13 00:02:36 +0200179 elseif s:is_comment(line) && s:is_empty(getline(v:lnum-1))
Christian Brabandt711f4a02025-03-18 22:36:28 +0100180 if s:is_in_block(v:lnum)
181 " return indent of line in same block
182 return indent(lnum)
183 else
184 " use indent of current line
185 return indent(v:lnum)
186 endif
Bram Moolenaar071d4272004-06-13 20:20:40 +0000187 endif
188
Bram Moolenaar7db25fe2018-05-13 00:02:36 +0200189 return ind > 0 ? ind : 0
Bram Moolenaar071d4272004-06-13 20:20:40 +0000190endfunction
191
Bram Moolenaar5c736222010-01-06 20:54:52 +0100192function! s:is_continuation_line(line)
Bram Moolenaar7db25fe2018-05-13 00:02:36 +0200193 " Comment, cannot be a line continuation
194 if a:line =~ '^\s*#'
195 return 0
196 else
197 " start-of-line
198 " \\ or && or || or |
199 " followed optionally by { or #
200 return a:line =~ '\%(\%(^\|[^\\]\)\\\|&&\|||\||\)' .
Bram Moolenaar1ccd8ff2017-08-11 19:50:37 +0200201 \ '\s*\({\s*\)\=\(#.*\)\=$'
Bram Moolenaar7db25fe2018-05-13 00:02:36 +0200202 endif
Bram Moolenaar5c736222010-01-06 20:54:52 +0100203endfunction
204
205function! s:find_continued_lnum(lnum)
206 let i = a:lnum
207 while i > 1 && s:is_continuation_line(getline(i - 1))
208 let i -= 1
209 endwhile
210 return i
211endfunction
212
Bram Moolenaar7db25fe2018-05-13 00:02:36 +0200213function! s:is_function_definition(line)
214 return a:line =~ '^\s*\<\k\+\>\s*()\s*{' ||
215 \ a:line =~ '^\s*{' ||
Lukas Zapletal0acd3ab2024-11-14 21:50:15 +0100216 \ a:line =~ '^\s*function\s*\k\+\s*\%(()\)\?\s*{' ||
217 \ ((&ft is# 'zsh' || s:is_bash()) &&
218 \ a:line =~ '^\s*function\s*\S\+\s*\%(()\)\?\s*{' )
Bram Moolenaar7db25fe2018-05-13 00:02:36 +0200219endfunction
220
Bram Moolenaar62e1bb42019-04-08 16:25:07 +0200221function! s:is_array(line)
Christian Brabandt711f4a02025-03-18 22:36:28 +0100222 return a:line =~ '^\s*\(\(declare\|typeset\|local\)\s\+\(-[Aalrtu]\+\s\+\)\?\)\?\<\k\+\>=('
223endfunction
224
225function! s:is_in_block(line)
226 " checks whether a:line is whithin a
227 " block e.g. a shell function
228 " foo() {
229 " ..
230 " }
231 let prevline = searchpair('{', '', '}', 'bnW', 'synIDattr(synID(line("."),col("."), 1),"name") =~? "comment\\|quote"')
232 let nextline = searchpair('{', '', '}', 'nW', 'synIDattr(synID(line("."),col("."), 1),"name") =~? "comment\\|quote"')
233 return a:line > prevline && a:line < nextline
Bram Moolenaar62e1bb42019-04-08 16:25:07 +0200234endfunction
235
Bram Moolenaar5c736222010-01-06 20:54:52 +0100236function! s:is_case_label(line, pnum)
237 if a:line !~ '^\s*(\=.*)'
238 return 0
239 endif
240
241 if a:pnum > 0
242 let pine = getline(a:pnum)
243 if !(s:is_case(pine) || s:is_case_ended(pine))
244 return 0
245 endif
246 endif
247
248 let suffix = substitute(a:line, '^\s*(\=', "", "")
249 let nesting = 0
250 let i = 0
251 let n = strlen(suffix)
252 while i < n
253 let c = suffix[i]
254 let i += 1
255 if c == '\\'
256 let i += 1
257 elseif c == '('
258 let nesting += 1
259 elseif c == ')'
260 if nesting == 0
261 return 1
262 endif
263 let nesting -= 1
264 endif
265 endwhile
266 return 0
267endfunction
268
269function! s:is_case(line)
270 return a:line =~ '^\s*case\>'
271endfunction
272
273function! s:is_case_break(line)
274 return a:line =~ '^\s*;[;&]'
275endfunction
276
Bram Moolenaare18dbe82016-07-02 21:42:23 +0200277function! s:is_here_doc(line)
278 if a:line =~ '^\w\+$'
Bram Moolenaar62e1bb42019-04-08 16:25:07 +0200279 let here_pat = '<<-\?'. s:escape(a:line). '\$'
280 return search(here_pat, 'bnW') > 0
Bram Moolenaare18dbe82016-07-02 21:42:23 +0200281 endif
282 return 0
283endfunction
284
Bram Moolenaar5c736222010-01-06 20:54:52 +0100285function! s:is_case_ended(line)
286 return s:is_case_break(a:line) || a:line =~ ';[;&]\s*\%(#.*\)\=$'
287endfunction
288
Bram Moolenaardfb18412013-12-11 18:53:29 +0100289function! s:is_case_empty(line)
290 if a:line =~ '^\s*$' || a:line =~ '^\s*#'
291 return s:is_case_empty(getline(v:lnum - 1))
292 else
293 return a:line =~ '^\s*case\>'
294 endif
295endfunction
296
Bram Moolenaare18dbe82016-07-02 21:42:23 +0200297function! s:escape(pattern)
298 return '\V'. escape(a:pattern, '\\')
299endfunction
300
Bram Moolenaar7db25fe2018-05-13 00:02:36 +0200301function! s:is_empty(line)
302 return a:line =~ '^\s*$'
303endfunction
304
305function! s:end_block(line)
306 return a:line =~ '^\s*}'
307endfunction
308
309function! s:start_block(line)
Christian Brabandt711f4a02025-03-18 22:36:28 +0100310 return a:line =~ '^[^#]*[{(]\s*\(#.*\)\?$'
Bram Moolenaar7db25fe2018-05-13 00:02:36 +0200311endfunction
312
313function! s:is_comment(line)
314 return a:line =~ '^\s*#'
315endfunction
316
Bram Moolenaar62e1bb42019-04-08 16:25:07 +0200317function! s:is_end_expression(line)
318 return a:line =~ '\<\%(fi\|esac\|done\|end\)\>\s*\%(#.*\)\=$'
319endfunction
320
321function! s:is_bash()
Christian Brabandt711f4a02025-03-18 22:36:28 +0100322 if &ft is# 'bash' || getline(1) is# '#!/bin/bash'
323 return v:true
324 else
325 return get(g:, 'is_bash', 0) || get(b:, 'is_bash', 0)
326 endif
Bram Moolenaar62e1bb42019-04-08 16:25:07 +0200327endfunction
328
Bram Moolenaar42eeac32005-06-29 22:40:58 +0000329let &cpo = s:cpo_save
330unlet s:cpo_save