1 // Scintilla source code edit control
2 /** @file LexBash.cxx
3  ** Lexer for Bash.
4  **/
5 // Copyright 2004-2005 by Neil Hodgson <neilh@scintilla.org>
6 // Adapted from LexPerl by Kein-Hong Man <mkh@pl.jaring.my> 2004
7 // The License.txt file describes the conditions under which this software may be distributed.
8 
9 #include <stdlib.h>
10 #include <string.h>
11 #include <ctype.h>
12 #include <stdio.h>
13 #include <stdarg.h>
14 
15 #include "Platform.h"
16 
17 #include "PropSet.h"
18 #include "Accessor.h"
19 #include "KeyWords.h"
20 #include "Scintilla.h"
21 #include "SciLexer.h"
22 
23 #define BASH_BASE_ERROR		65
24 #define BASH_BASE_DECIMAL	66
25 #define BASH_BASE_HEX		67
26 #define BASH_BASE_OCTAL		68
27 #define BASH_BASE_OCTAL_ERROR	69
28 
29 #define HERE_DELIM_MAX 256
30 
translateBashDigit(char ch)31 static inline int translateBashDigit(char ch) {
32 	if (ch >= '0' && ch <= '9') {
33 		return ch - '0';
34 	} else if (ch >= 'a' && ch <= 'z') {
35 		return ch - 'a' + 10;
36 	} else if (ch >= 'A' && ch <= 'Z') {
37 		return ch - 'A' + 36;
38 	} else if (ch == '@') {
39 		return 62;
40 	} else if (ch == '_') {
41 		return 63;
42 	}
43 	return BASH_BASE_ERROR;
44 }
45 
isEOLChar(char ch)46 static inline bool isEOLChar(char ch) {
47 	return (ch == '\r') || (ch == '\n');
48 }
49 
isSingleCharOp(char ch)50 static bool isSingleCharOp(char ch) {
51 	char strCharSet[2];
52 	strCharSet[0] = ch;
53 	strCharSet[1] = '\0';
54 	return (NULL != strstr("rwxoRWXOezsfdlpSbctugkTBMACahGLNn", strCharSet));
55 }
56 
isBashOperator(char ch)57 static inline bool isBashOperator(char ch) {
58 	if (ch == '^' || ch == '&' || ch == '\\' || ch == '%' ||
59 	        ch == '(' || ch == ')' || ch == '-' || ch == '+' ||
60 	        ch == '=' || ch == '|' || ch == '{' || ch == '}' ||
61 	        ch == '[' || ch == ']' || ch == ':' || ch == ';' ||
62 	        ch == '>' || ch == ',' || ch == '/' || ch == '<' ||
63 	        ch == '?' || ch == '!' || ch == '.' || ch == '~' ||
64 		ch == '@')
65 		return true;
66 	return false;
67 }
68 
classifyWordBash(unsigned int start,unsigned int end,WordList & keywords,Accessor & styler)69 static int classifyWordBash(unsigned int start, unsigned int end, WordList &keywords, Accessor &styler) {
70 	char s[100];
71 	for (unsigned int i = 0; i < end - start + 1 && i < 30; i++) {
72 		s[i] = styler[start + i];
73 		s[i + 1] = '\0';
74 	}
75 	char chAttr = SCE_SH_IDENTIFIER;
76 	if (keywords.InList(s))
77 		chAttr = SCE_SH_WORD;
78 	styler.ColourTo(end, chAttr);
79 	return chAttr;
80 }
81 
getBashNumberBase(unsigned int start,unsigned int end,Accessor & styler)82 static inline int getBashNumberBase(unsigned int start, unsigned int end, Accessor &styler) {
83 	int base = 0;
84 	for (unsigned int i = 0; i < end - start + 1 && i < 10; i++) {
85 		base = base * 10 + (styler[start + i] - '0');
86 	}
87 	if (base > 64 || (end - start) > 1) {
88 		return BASH_BASE_ERROR;
89 	}
90 	return base;
91 }
92 
isEndVar(char ch)93 static inline bool isEndVar(char ch) {
94 	return !isalnum(ch) && ch != '$' && ch != '_';
95 }
96 
isNonQuote(char ch)97 static inline bool isNonQuote(char ch) {
98 	return isalnum(ch) || ch == '_';
99 }
100 
isMatch(Accessor & styler,int lengthDoc,int pos,const char * val)101 static bool isMatch(Accessor &styler, int lengthDoc, int pos, const char *val) {
102 	if ((pos + static_cast<int>(strlen(val))) >= lengthDoc) {
103 		return false;
104 	}
105 	while (*val) {
106 		if (*val != styler[pos++]) {
107 			return false;
108 		}
109 		val++;
110 	}
111 	return true;
112 }
113 
opposite(char ch)114 static char opposite(char ch) {
115 	if (ch == '(')
116 		return ')';
117 	if (ch == '[')
118 		return ']';
119 	if (ch == '{')
120 		return '}';
121 	if (ch == '<')
122 		return '>';
123 	return ch;
124 }
125 
ColouriseBashDoc(unsigned int startPos,int length,int initStyle,WordList * keywordlists[],Accessor & styler)126 static void ColouriseBashDoc(unsigned int startPos, int length, int initStyle,
127                              WordList *keywordlists[], Accessor &styler) {
128 
129 	// Lexer for bash often has to backtrack to start of current style to determine
130 	// which characters are being used as quotes, how deeply nested is the
131 	// start position and what the termination string is for here documents
132 
133 	WordList &keywords = *keywordlists[0];
134 
135 	class HereDocCls {
136 	public:
137 		int State;		// 0: '<<' encountered
138 		// 1: collect the delimiter
139 		// 2: here doc text (lines after the delimiter)
140 		char Quote;		// the char after '<<'
141 		bool Quoted;		// true if Quote in ('\'','"','`')
142 		bool Indent;		// indented delimiter (for <<-)
143 		int DelimiterLength;	// strlen(Delimiter)
144 		char *Delimiter;	// the Delimiter, 256: sizeof PL_tokenbuf
145 		HereDocCls() {
146 			State = 0;
147             Quote = 0;
148             Quoted = false;
149             Indent = 0;
150 			DelimiterLength = 0;
151 			Delimiter = new char[HERE_DELIM_MAX];
152 			Delimiter[0] = '\0';
153 		}
154 		~HereDocCls() {
155 			delete []Delimiter;
156 		}
157 	};
158 	HereDocCls HereDoc;
159 
160 	class QuoteCls {
161 		public:
162 		int  Rep;
163 		int  Count;
164 		char Up;
165 		char Down;
166 		QuoteCls() {
167 			this->New(1);
168 		}
169 		void New(int r) {
170 			Rep   = r;
171 			Count = 0;
172 			Up    = '\0';
173 			Down  = '\0';
174 		}
175 		void Open(char u) {
176 			Count++;
177 			Up    = u;
178 			Down  = opposite(Up);
179 		}
180 	};
181 	QuoteCls Quote;
182 
183 	int state = initStyle;
184 	int numBase = 0;
185 	unsigned int lengthDoc = startPos + length;
186 
187 	// If in a long distance lexical state, seek to the beginning to find quote characters
188 	// Bash strings can be multi-line with embedded newlines, so backtrack.
189 	// Bash numbers have additional state during lexing, so backtrack too.
190 	if (state == SCE_SH_HERE_Q) {
191 		while ((startPos > 1) && (styler.StyleAt(startPos) != SCE_SH_HERE_DELIM)) {
192 			startPos--;
193 		}
194 		startPos = styler.LineStart(styler.GetLine(startPos));
195 		state = styler.StyleAt(startPos - 1);
196 	}
197 	if (state == SCE_SH_STRING
198 	 || state == SCE_SH_BACKTICKS
199 	 || state == SCE_SH_CHARACTER
200 	 || state == SCE_SH_NUMBER
201 	 || state == SCE_SH_IDENTIFIER
202 	 || state == SCE_SH_COMMENTLINE
203 	) {
204 		while ((startPos > 1) && (styler.StyleAt(startPos - 1) == state)) {
205 			startPos--;
206 		}
207 		state = SCE_SH_DEFAULT;
208 	}
209 
210 	styler.StartAt(startPos);
211 	char chPrev = styler.SafeGetCharAt(startPos - 1);
212 	if (startPos == 0)
213 		chPrev = '\n';
214 	char chNext = styler[startPos];
215 	styler.StartSegment(startPos);
216 
217 	for (unsigned int i = startPos; i < lengthDoc; i++) {
218 		char ch = chNext;
219 		// if the current character is not consumed due to the completion of an
220 		// earlier style, lexing can be restarted via a simple goto
221 	restartLexer:
222 		chNext = styler.SafeGetCharAt(i + 1);
223 		char chNext2 = styler.SafeGetCharAt(i + 2);
224 
225 		if (styler.IsLeadByte(ch)) {
226 			chNext = styler.SafeGetCharAt(i + 2);
227 			chPrev = ' ';
228 			i += 1;
229 			continue;
230 		}
231 
232 		if ((chPrev == '\r' && ch == '\n')) {	// skip on DOS/Windows
233 			styler.ColourTo(i, state);
234 			chPrev = ch;
235 			continue;
236 		}
237 
238 		if (HereDoc.State == 1 && isEOLChar(ch)) {
239 			// Begin of here-doc (the line after the here-doc delimiter):
240 			// Lexically, the here-doc starts from the next line after the >>, but the
241 			// first line of here-doc seem to follow the style of the last EOL sequence
242 			HereDoc.State = 2;
243 			if (HereDoc.Quoted) {
244 				if (state == SCE_SH_HERE_DELIM) {
245 					// Missing quote at end of string! We are stricter than bash.
246 					// Colour here-doc anyway while marking this bit as an error.
247 					state = SCE_SH_ERROR;
248 				}
249 				styler.ColourTo(i - 1, state);
250 				// HereDoc.Quote always == '\''
251 				state = SCE_SH_HERE_Q;
252 			} else {
253 				styler.ColourTo(i - 1, state);
254 				// always switch
255 				state = SCE_SH_HERE_Q;
256 			}
257 		}
258 
259 		if (state == SCE_SH_DEFAULT) {
260 			if (ch == '\\') {	// escaped character
261 				if (i < lengthDoc - 1)
262 					i++;
263 				ch = chNext;
264 				chNext = chNext2;
265 				styler.ColourTo(i, SCE_SH_IDENTIFIER);
266 			} else if (isdigit(ch)) {
267 				state = SCE_SH_NUMBER;
268 				numBase = BASH_BASE_DECIMAL;
269 				if (ch == '0') {	// hex,octal
270 					if (chNext == 'x' || chNext == 'X') {
271 						numBase = BASH_BASE_HEX;
272 						i++;
273 						ch = chNext;
274 						chNext = chNext2;
275 					} else if (isdigit(chNext)) {
276 						numBase = BASH_BASE_OCTAL;
277 					}
278 				}
279 			} else if (iswordstart(ch)) {
280 				state = SCE_SH_WORD;
281 				if (!iswordchar(chNext) && chNext != '+' && chNext != '-') {
282 					// We need that if length of word == 1!
283 					// This test is copied from the SCE_SH_WORD handler.
284 					classifyWordBash(styler.GetStartSegment(), i, keywords, styler);
285 					state = SCE_SH_DEFAULT;
286 				}
287 			} else if (ch == '#') {
288 				state = SCE_SH_COMMENTLINE;
289 			} else if (ch == '\"') {
290 				state = SCE_SH_STRING;
291 				Quote.New(1);
292 				Quote.Open(ch);
293 			} else if (ch == '\'') {
294 				state = SCE_SH_CHARACTER;
295 				Quote.New(1);
296 				Quote.Open(ch);
297 			} else if (ch == '`') {
298 				state = SCE_SH_BACKTICKS;
299 				Quote.New(1);
300 				Quote.Open(ch);
301 			} else if (ch == '$') {
302 				if (chNext == '{') {
303 					state = SCE_SH_PARAM;
304 					goto startQuote;
305 				} else if (chNext == '\'') {
306 					state = SCE_SH_CHARACTER;
307 					goto startQuote;
308 				} else if (chNext == '"') {
309 					state = SCE_SH_STRING;
310 					goto startQuote;
311 				} else if (chNext == '(' && chNext2 == '(') {
312 					styler.ColourTo(i, SCE_SH_OPERATOR);
313 					state = SCE_SH_DEFAULT;
314 					goto skipChar;
315 				} else if (chNext == '(' || chNext == '`') {
316 					state = SCE_SH_BACKTICKS;
317 				startQuote:
318 					Quote.New(1);
319 					Quote.Open(chNext);
320 					goto skipChar;
321 				} else {
322 					state = SCE_SH_SCALAR;
323 				skipChar:
324 					i++;
325 					ch = chNext;
326 					chNext = chNext2;
327 				}
328 			} else if (ch == '*') {
329 				if (chNext == '*') {	// exponentiation
330 					i++;
331 					ch = chNext;
332 					chNext = chNext2;
333 				}
334 				styler.ColourTo(i, SCE_SH_OPERATOR);
335 			} else if (ch == '<' && chNext == '<') {
336 				state = SCE_SH_HERE_DELIM;
337 				HereDoc.State = 0;
338 				HereDoc.Indent = false;
339 			} else if (ch == '-'	// file test operators
340 			           && isSingleCharOp(chNext)
341 			           && !isalnum((chNext2 = styler.SafeGetCharAt(i+2)))) {
342 				styler.ColourTo(i + 1, SCE_SH_WORD);
343 				state = SCE_SH_DEFAULT;
344 				i++;
345 				ch = chNext;
346 				chNext = chNext2;
347 			} else if (isBashOperator(ch)) {
348 				styler.ColourTo(i, SCE_SH_OPERATOR);
349 			} else {
350 				// keep colouring defaults to make restart easier
351 				styler.ColourTo(i, SCE_SH_DEFAULT);
352 			}
353 		} else if (state == SCE_SH_NUMBER) {
354 			int digit = translateBashDigit(ch);
355 			if (numBase == BASH_BASE_DECIMAL) {
356 				if (ch == '#') {
357 					numBase = getBashNumberBase(styler.GetStartSegment(), i - 1, styler);
358 					if (numBase == BASH_BASE_ERROR)	// take the rest as comment
359 						goto numAtEnd;
360 				} else if (!isdigit(ch))
361 					goto numAtEnd;
362 			} else if (numBase == BASH_BASE_HEX) {
363 				if ((digit < 16) || (digit >= 36 && digit <= 41)) {
364 					// hex digit 0-9a-fA-F
365 				} else
366 					goto numAtEnd;
367 			} else if (numBase == BASH_BASE_OCTAL ||
368 				   numBase == BASH_BASE_OCTAL_ERROR) {
369 				if (digit > 7) {
370 					if (digit <= 9) {
371 						numBase = BASH_BASE_OCTAL_ERROR;
372 					} else
373 						goto numAtEnd;
374 				}
375 			} else if (numBase == BASH_BASE_ERROR) {
376 				if (digit > 9)
377 					goto numAtEnd;
378 			} else {	// DD#DDDD number style handling
379 				if (digit != BASH_BASE_ERROR) {
380 					if (numBase <= 36) {
381 						// case-insensitive if base<=36
382 						if (digit >= 36) digit -= 26;
383 					}
384 					if (digit >= numBase) {
385 						if (digit <= 9) {
386 							numBase = BASH_BASE_ERROR;
387 						} else
388 							goto numAtEnd;
389 					}
390 				} else {
391 			numAtEnd:
392 					if (numBase == BASH_BASE_ERROR ||
393 					    numBase == BASH_BASE_OCTAL_ERROR)
394 						state = SCE_SH_ERROR;
395 					styler.ColourTo(i - 1, state);
396 					state = SCE_SH_DEFAULT;
397 					goto restartLexer;
398 				}
399 			}
400 		} else if (state == SCE_SH_WORD) {
401 			if (!iswordchar(chNext) && chNext != '+' && chNext != '-') {
402 				// "." never used in Bash variable names
403 				// but used in file names
404 				classifyWordBash(styler.GetStartSegment(), i, keywords, styler);
405 				state = SCE_SH_DEFAULT;
406 				ch = ' ';
407 			}
408 		} else if (state == SCE_SH_IDENTIFIER) {
409 			if (!iswordchar(chNext) && chNext != '+' && chNext != '-') {
410 				styler.ColourTo(i, SCE_SH_IDENTIFIER);
411 				state = SCE_SH_DEFAULT;
412 				ch = ' ';
413 			}
414 		} else {
415 			if (state == SCE_SH_COMMENTLINE) {
416 				if (ch == '\\' && isEOLChar(chNext)) {
417 					// comment continuation
418 					if (chNext == '\r' && chNext2 == '\n') {
419 						i += 2;
420 						ch = styler.SafeGetCharAt(i);
421 						chNext = styler.SafeGetCharAt(i + 1);
422 					} else {
423 						i++;
424 						ch = chNext;
425 						chNext = chNext2;
426 					}
427 				} else if (isEOLChar(ch)) {
428 					styler.ColourTo(i - 1, state);
429 					state = SCE_SH_DEFAULT;
430 					goto restartLexer;
431 				} else if (isEOLChar(chNext)) {
432 					styler.ColourTo(i, state);
433 					state = SCE_SH_DEFAULT;
434 				}
435 			} else if (state == SCE_SH_HERE_DELIM) {
436 				//
437 				// From Bash info:
438 				// ---------------
439 				// Specifier format is: <<[-]WORD
440 				// Optional '-' is for removal of leading tabs from here-doc.
441 				// Whitespace acceptable after <<[-] operator
442 				//
443 				if (HereDoc.State == 0) { // '<<' encountered
444 					HereDoc.State = 1;
445 					HereDoc.Quote = chNext;
446 					HereDoc.Quoted = false;
447 					HereDoc.DelimiterLength = 0;
448 					HereDoc.Delimiter[HereDoc.DelimiterLength] = '\0';
449 					if (chNext == '\'' || chNext == '\"') {	// a quoted here-doc delimiter (' or ")
450 						i++;
451 						ch = chNext;
452 						chNext = chNext2;
453 						HereDoc.Quoted = true;
454 					} else if (!HereDoc.Indent && chNext == '-') {	// <<- indent case
455 						HereDoc.Indent = true;
456 						HereDoc.State = 0;
457 					} else if (isalpha(chNext) || chNext == '_' || chNext == '\\'
458 						|| chNext == '-' || chNext == '+' || chNext == '!') {
459 						// an unquoted here-doc delimiter, no special handling
460                         // TODO check what exactly bash considers part of the delim
461 					} else if (chNext == '<') {	// HERE string <<<
462 						i++;
463 						ch = chNext;
464 						chNext = chNext2;
465 						styler.ColourTo(i, SCE_SH_HERE_DELIM);
466 						state = SCE_SH_DEFAULT;
467 						HereDoc.State = 0;
468 					} else if (isspacechar(chNext)) {
469 						// eat whitespace
470 						HereDoc.State = 0;
471 					} else if (isdigit(chNext) || chNext == '=' || chNext == '$') {
472 						// left shift << or <<= operator cases
473 						styler.ColourTo(i, SCE_SH_OPERATOR);
474 						state = SCE_SH_DEFAULT;
475 						HereDoc.State = 0;
476 					} else {
477 						// symbols terminates; deprecated zero-length delimiter
478 					}
479 				} else if (HereDoc.State == 1) { // collect the delimiter
480 					if (HereDoc.Quoted) { // a quoted here-doc delimiter
481 						if (ch == HereDoc.Quote) { // closing quote => end of delimiter
482 							styler.ColourTo(i, state);
483 							state = SCE_SH_DEFAULT;
484 						} else {
485 							if (ch == '\\' && chNext == HereDoc.Quote) { // escaped quote
486 								i++;
487 								ch = chNext;
488 								chNext = chNext2;
489 							}
490 							HereDoc.Delimiter[HereDoc.DelimiterLength++] = ch;
491 							HereDoc.Delimiter[HereDoc.DelimiterLength] = '\0';
492 						}
493 					} else { // an unquoted here-doc delimiter
494 						if (isalnum(ch) || ch == '_' || ch == '-' || ch == '+' || ch == '!') {
495 							HereDoc.Delimiter[HereDoc.DelimiterLength++] = ch;
496 							HereDoc.Delimiter[HereDoc.DelimiterLength] = '\0';
497 						} else if (ch == '\\') {
498 							// skip escape prefix
499 						} else {
500 							styler.ColourTo(i - 1, state);
501 							state = SCE_SH_DEFAULT;
502 							goto restartLexer;
503 						}
504 					}
505 					if (HereDoc.DelimiterLength >= HERE_DELIM_MAX - 1) {
506 						styler.ColourTo(i - 1, state);
507 						state = SCE_SH_ERROR;
508 						goto restartLexer;
509 					}
510 				}
511 			} else if (HereDoc.State == 2) {
512 				// state == SCE_SH_HERE_Q
513 				if (isMatch(styler, lengthDoc, i, HereDoc.Delimiter)) {
514 					if (!HereDoc.Indent && isEOLChar(chPrev)) {
515 					endHereDoc:
516 						// standard HERE delimiter
517 						i += HereDoc.DelimiterLength;
518 						chPrev = styler.SafeGetCharAt(i - 1);
519 						ch = styler.SafeGetCharAt(i);
520 						if (isEOLChar(ch)) {
521 							styler.ColourTo(i - 1, state);
522 							state = SCE_SH_DEFAULT;
523 							HereDoc.State = 0;
524 							goto restartLexer;
525 						}
526 						chNext = styler.SafeGetCharAt(i + 1);
527 					} else if (HereDoc.Indent) {
528 						// indented HERE delimiter
529 						unsigned int bk = (i > 0)? i - 1: 0;
530 						while (i > 0) {
531 							ch = styler.SafeGetCharAt(bk--);
532 							if (isEOLChar(ch)) {
533 								goto endHereDoc;
534 							} else if (!isspacechar(ch)) {
535 								break;	// got leading non-whitespace
536 							}
537 						}
538 					}
539 				}
540 			} else if (state == SCE_SH_SCALAR) {	// variable names
541 				if (isEndVar(ch)) {
542 					if ((state == SCE_SH_SCALAR)
543 					    && i == (styler.GetStartSegment() + 1)) {
544 						// Special variable: $(, $_ etc.
545 						styler.ColourTo(i, state);
546 						state = SCE_SH_DEFAULT;
547 					} else {
548 						styler.ColourTo(i - 1, state);
549 						state = SCE_SH_DEFAULT;
550 						goto restartLexer;
551 					}
552 				}
553 			} else if (state == SCE_SH_STRING
554 				|| state == SCE_SH_CHARACTER
555 				|| state == SCE_SH_BACKTICKS
556 				|| state == SCE_SH_PARAM
557 				) {
558 				if (!Quote.Down && !isspacechar(ch)) {
559 					Quote.Open(ch);
560 				} else if (ch == '\\' && Quote.Up != '\\') {
561 					i++;
562 					ch = chNext;
563 					chNext = styler.SafeGetCharAt(i + 1);
564 				} else if (ch == Quote.Down) {
565 					Quote.Count--;
566 					if (Quote.Count == 0) {
567 						Quote.Rep--;
568 						if (Quote.Rep <= 0) {
569 							styler.ColourTo(i, state);
570 							state = SCE_SH_DEFAULT;
571 							ch = ' ';
572 						}
573 						if (Quote.Up == Quote.Down) {
574 							Quote.Count++;
575 						}
576 					}
577 				} else if (ch == Quote.Up) {
578 					Quote.Count++;
579 				}
580 			}
581 		}
582 		if (state == SCE_SH_ERROR) {
583 			break;
584 		}
585 		chPrev = ch;
586 	}
587 	styler.ColourTo(lengthDoc - 1, state);
588 }
589 
IsCommentLine(int line,Accessor & styler)590 static bool IsCommentLine(int line, Accessor &styler) {
591 	int pos = styler.LineStart(line);
592 	int eol_pos = styler.LineStart(line + 1) - 1;
593 	for (int i = pos; i < eol_pos; i++) {
594 		char ch = styler[i];
595 		if (ch == '#')
596 			return true;
597 		else if (ch != ' ' && ch != '\t')
598 			return false;
599 	}
600 	return false;
601 }
602 
FoldBashDoc(unsigned int startPos,int length,int,WordList * [],Accessor & styler)603 static void FoldBashDoc(unsigned int startPos, int length, int, WordList *[],
604                             Accessor &styler) {
605 	bool foldComment = styler.GetPropertyInt("fold.comment") != 0;
606 	bool foldCompact = styler.GetPropertyInt("fold.compact", 1) != 0;
607 	unsigned int endPos = startPos + length;
608 	int visibleChars = 0;
609 	int lineCurrent = styler.GetLine(startPos);
610 	int levelPrev = styler.LevelAt(lineCurrent) & SC_FOLDLEVELNUMBERMASK;
611 	int levelCurrent = levelPrev;
612 	char chNext = styler[startPos];
613 	int styleNext = styler.StyleAt(startPos);
614 	for (unsigned int i = startPos; i < endPos; i++) {
615 		char ch = chNext;
616 		chNext = styler.SafeGetCharAt(i + 1);
617 		int style = styleNext;
618 		styleNext = styler.StyleAt(i + 1);
619 		bool atEOL = (ch == '\r' && chNext != '\n') || (ch == '\n');
620         // Comment folding
621 		if (foldComment && atEOL && IsCommentLine(lineCurrent, styler))
622         {
623             if (!IsCommentLine(lineCurrent - 1, styler)
624                 && IsCommentLine(lineCurrent + 1, styler))
625                 levelCurrent++;
626             else if (IsCommentLine(lineCurrent - 1, styler)
627                      && !IsCommentLine(lineCurrent+1, styler))
628                 levelCurrent--;
629         }
630 		if (style == SCE_SH_OPERATOR) {
631 			if (ch == '{') {
632 				levelCurrent++;
633 			} else if (ch == '}') {
634 				levelCurrent--;
635 			}
636 		}
637 		if (atEOL) {
638 			int lev = levelPrev;
639 			if (visibleChars == 0 && foldCompact)
640 				lev |= SC_FOLDLEVELWHITEFLAG;
641 			if ((levelCurrent > levelPrev) && (visibleChars > 0))
642 				lev |= SC_FOLDLEVELHEADERFLAG;
643 			if (lev != styler.LevelAt(lineCurrent)) {
644 				styler.SetLevel(lineCurrent, lev);
645 			}
646 			lineCurrent++;
647 			levelPrev = levelCurrent;
648 			visibleChars = 0;
649 		}
650 		if (!isspacechar(ch))
651 			visibleChars++;
652 	}
653 	// Fill in the real level of the next line, keeping the current flags as they will be filled in later
654 	int flagsNext = styler.LevelAt(lineCurrent) & ~SC_FOLDLEVELNUMBERMASK;
655 	styler.SetLevel(lineCurrent, levelPrev | flagsNext);
656 }
657 
658 static const char * const bashWordListDesc[] = {
659 	"Keywords",
660 	0
661 };
662 
663 LexerModule lmBash(SCLEX_BASH, ColouriseBashDoc, "bash", FoldBashDoc, bashWordListDesc);
664