Update Scintilla to version 3.5.3
[TortoiseGit.git] / ext / scintilla / lexers / LexBash.cxx
blobb495575d8d401a193efb7884bfe23a40f2d3b558
1 // Scintilla source code edit control
2 /** @file LexBash.cxx
3 ** Lexer for Bash.
4 **/
5 // Copyright 2004-2012 by Neil Hodgson <neilh@scintilla.org>
6 // Adapted from LexPerl by Kein-Hong Man 2004
7 // The License.txt file describes the conditions under which this software may be distributed.
9 #include <stdlib.h>
10 #include <string.h>
11 #include <stdio.h>
12 #include <stdarg.h>
13 #include <assert.h>
15 #include "ILexer.h"
16 #include "Scintilla.h"
17 #include "SciLexer.h"
19 #include "WordList.h"
20 #include "LexAccessor.h"
21 #include "Accessor.h"
22 #include "StyleContext.h"
23 #include "CharacterSet.h"
24 #include "LexerModule.h"
26 #ifdef SCI_NAMESPACE
27 using namespace Scintilla;
28 #endif
30 #define HERE_DELIM_MAX 256
32 // define this if you want 'invalid octals' to be marked as errors
33 // usually, this is not a good idea, permissive lexing is better
34 #undef PEDANTIC_OCTAL
36 #define BASH_BASE_ERROR 65
37 #define BASH_BASE_DECIMAL 66
38 #define BASH_BASE_HEX 67
39 #ifdef PEDANTIC_OCTAL
40 #define BASH_BASE_OCTAL 68
41 #define BASH_BASE_OCTAL_ERROR 69
42 #endif
44 // state constants for parts of a bash command segment
45 #define BASH_CMD_BODY 0
46 #define BASH_CMD_START 1
47 #define BASH_CMD_WORD 2
48 #define BASH_CMD_TEST 3
49 #define BASH_CMD_ARITH 4
50 #define BASH_CMD_DELIM 5
52 // state constants for nested delimiter pairs, used by
53 // SCE_SH_STRING and SCE_SH_BACKTICKS processing
54 #define BASH_DELIM_LITERAL 0
55 #define BASH_DELIM_STRING 1
56 #define BASH_DELIM_CSTRING 2
57 #define BASH_DELIM_LSTRING 3
58 #define BASH_DELIM_COMMAND 4
59 #define BASH_DELIM_BACKTICK 5
61 #define BASH_DELIM_STACK_MAX 7
63 static inline int translateBashDigit(int ch) {
64 if (ch >= '0' && ch <= '9') {
65 return ch - '0';
66 } else if (ch >= 'a' && ch <= 'z') {
67 return ch - 'a' + 10;
68 } else if (ch >= 'A' && ch <= 'Z') {
69 return ch - 'A' + 36;
70 } else if (ch == '@') {
71 return 62;
72 } else if (ch == '_') {
73 return 63;
75 return BASH_BASE_ERROR;
78 static inline int getBashNumberBase(char *s) {
79 int i = 0;
80 int base = 0;
81 while (*s) {
82 base = base * 10 + (*s++ - '0');
83 i++;
85 if (base > 64 || i > 2) {
86 return BASH_BASE_ERROR;
88 return base;
91 static int opposite(int ch) {
92 if (ch == '(') return ')';
93 if (ch == '[') return ']';
94 if (ch == '{') return '}';
95 if (ch == '<') return '>';
96 return ch;
99 static void ColouriseBashDoc(unsigned int startPos, int length, int initStyle,
100 WordList *keywordlists[], Accessor &styler) {
102 WordList &keywords = *keywordlists[0];
103 WordList cmdDelimiter, bashStruct, bashStruct_in;
104 cmdDelimiter.Set("| || |& & && ; ;; ( ) { }");
105 bashStruct.Set("if elif fi while until else then do done esac eval");
106 bashStruct_in.Set("for case select");
108 CharacterSet setWordStart(CharacterSet::setAlpha, "_");
109 // note that [+-] are often parts of identifiers in shell scripts
110 CharacterSet setWord(CharacterSet::setAlphaNum, "._+-");
111 CharacterSet setMetaCharacter(CharacterSet::setNone, "|&;()<> \t\r\n");
112 setMetaCharacter.Add(0);
113 CharacterSet setBashOperator(CharacterSet::setNone, "^&%()-+=|{}[]:;>,*/<?!.~@");
114 CharacterSet setSingleCharOp(CharacterSet::setNone, "rwxoRWXOezsfdlpSbctugkTBMACahGLNn");
115 CharacterSet setParam(CharacterSet::setAlphaNum, "$_");
116 CharacterSet setHereDoc(CharacterSet::setAlpha, "_\\-+!");
117 CharacterSet setHereDoc2(CharacterSet::setAlphaNum, "_-+!");
118 CharacterSet setLeftShift(CharacterSet::setDigits, "=$");
120 class HereDocCls { // Class to manage HERE document elements
121 public:
122 int State; // 0: '<<' encountered
123 // 1: collect the delimiter
124 // 2: here doc text (lines after the delimiter)
125 int Quote; // the char after '<<'
126 bool Quoted; // true if Quote in ('\'','"','`')
127 bool Indent; // indented delimiter (for <<-)
128 int DelimiterLength; // strlen(Delimiter)
129 char *Delimiter; // the Delimiter, 256: sizeof PL_tokenbuf
130 HereDocCls() {
131 State = 0;
132 Quote = 0;
133 Quoted = false;
134 Indent = 0;
135 DelimiterLength = 0;
136 Delimiter = new char[HERE_DELIM_MAX];
137 Delimiter[0] = '\0';
139 void Append(int ch) {
140 Delimiter[DelimiterLength++] = static_cast<char>(ch);
141 Delimiter[DelimiterLength] = '\0';
143 ~HereDocCls() {
144 delete []Delimiter;
147 HereDocCls HereDoc;
149 class QuoteCls { // Class to manage quote pairs (simplified vs LexPerl)
150 public:
151 int Count;
152 int Up, Down;
153 QuoteCls() {
154 Count = 0;
155 Up = '\0';
156 Down = '\0';
158 void Open(int u) {
159 Count++;
160 Up = u;
161 Down = opposite(Up);
163 void Start(int u) {
164 Count = 0;
165 Open(u);
168 QuoteCls Quote;
170 class QuoteStackCls { // Class to manage quote pairs that nest
171 public:
172 int Count;
173 int Up, Down;
174 int Style;
175 int Depth; // levels pushed
176 int *CountStack;
177 int *UpStack;
178 int *StyleStack;
179 QuoteStackCls() {
180 Count = 0;
181 Up = '\0';
182 Down = '\0';
183 Style = 0;
184 Depth = 0;
185 CountStack = new int[BASH_DELIM_STACK_MAX];
186 UpStack = new int[BASH_DELIM_STACK_MAX];
187 StyleStack = new int[BASH_DELIM_STACK_MAX];
189 void Start(int u, int s) {
190 Count = 1;
191 Up = u;
192 Down = opposite(Up);
193 Style = s;
195 void Push(int u, int s) {
196 if (Depth >= BASH_DELIM_STACK_MAX)
197 return;
198 CountStack[Depth] = Count;
199 UpStack [Depth] = Up;
200 StyleStack[Depth] = Style;
201 Depth++;
202 Count = 1;
203 Up = u;
204 Down = opposite(Up);
205 Style = s;
207 void Pop(void) {
208 if (Depth <= 0)
209 return;
210 Depth--;
211 Count = CountStack[Depth];
212 Up = UpStack [Depth];
213 Style = StyleStack[Depth];
214 Down = opposite(Up);
216 ~QuoteStackCls() {
217 delete []CountStack;
218 delete []UpStack;
219 delete []StyleStack;
222 QuoteStackCls QuoteStack;
224 int numBase = 0;
225 int digit;
226 unsigned int endPos = startPos + length;
227 int cmdState = BASH_CMD_START;
228 int testExprType = 0;
230 // Always backtracks to the start of a line that is not a continuation
231 // of the previous line (i.e. start of a bash command segment)
232 int ln = styler.GetLine(startPos);
233 if (ln > 0 && startPos == static_cast<unsigned int>(styler.LineStart(ln)))
234 ln--;
235 for (;;) {
236 startPos = styler.LineStart(ln);
237 if (ln == 0 || styler.GetLineState(ln) == BASH_CMD_START)
238 break;
239 ln--;
241 initStyle = SCE_SH_DEFAULT;
243 StyleContext sc(startPos, endPos - startPos, initStyle, styler);
245 for (; sc.More(); sc.Forward()) {
247 // handle line continuation, updates per-line stored state
248 if (sc.atLineStart) {
249 ln = styler.GetLine(sc.currentPos);
250 if (sc.state == SCE_SH_STRING
251 || sc.state == SCE_SH_BACKTICKS
252 || sc.state == SCE_SH_CHARACTER
253 || sc.state == SCE_SH_HERE_Q
254 || sc.state == SCE_SH_COMMENTLINE
255 || sc.state == SCE_SH_PARAM) {
256 // force backtrack while retaining cmdState
257 styler.SetLineState(ln, BASH_CMD_BODY);
258 } else {
259 if (ln > 0) {
260 if ((sc.GetRelative(-3) == '\\' && sc.GetRelative(-2) == '\r' && sc.chPrev == '\n')
261 || sc.GetRelative(-2) == '\\') { // handle '\' line continuation
262 // retain last line's state
263 } else
264 cmdState = BASH_CMD_START;
266 styler.SetLineState(ln, cmdState);
270 // controls change of cmdState at the end of a non-whitespace element
271 // states BODY|TEST|ARITH persist until the end of a command segment
272 // state WORD persist, but ends with 'in' or 'do' construct keywords
273 int cmdStateNew = BASH_CMD_BODY;
274 if (cmdState == BASH_CMD_TEST || cmdState == BASH_CMD_ARITH || cmdState == BASH_CMD_WORD)
275 cmdStateNew = cmdState;
276 int stylePrev = sc.state;
278 // Determine if the current state should terminate.
279 switch (sc.state) {
280 case SCE_SH_OPERATOR:
281 sc.SetState(SCE_SH_DEFAULT);
282 if (cmdState == BASH_CMD_DELIM) // if command delimiter, start new command
283 cmdStateNew = BASH_CMD_START;
284 else if (sc.chPrev == '\\') // propagate command state if line continued
285 cmdStateNew = cmdState;
286 break;
287 case SCE_SH_WORD:
288 // "." never used in Bash variable names but used in file names
289 if (!setWord.Contains(sc.ch)) {
290 char s[500];
291 char s2[10];
292 sc.GetCurrent(s, sizeof(s));
293 // allow keywords ending in a whitespace or command delimiter
294 s2[0] = static_cast<char>(sc.ch);
295 s2[1] = '\0';
296 bool keywordEnds = IsASpace(sc.ch) || cmdDelimiter.InList(s2);
297 // 'in' or 'do' may be construct keywords
298 if (cmdState == BASH_CMD_WORD) {
299 if (strcmp(s, "in") == 0 && keywordEnds)
300 cmdStateNew = BASH_CMD_BODY;
301 else if (strcmp(s, "do") == 0 && keywordEnds)
302 cmdStateNew = BASH_CMD_START;
303 else
304 sc.ChangeState(SCE_SH_IDENTIFIER);
305 sc.SetState(SCE_SH_DEFAULT);
306 break;
308 // a 'test' keyword starts a test expression
309 if (strcmp(s, "test") == 0) {
310 if (cmdState == BASH_CMD_START && keywordEnds) {
311 cmdStateNew = BASH_CMD_TEST;
312 testExprType = 0;
313 } else
314 sc.ChangeState(SCE_SH_IDENTIFIER);
316 // detect bash construct keywords
317 else if (bashStruct.InList(s)) {
318 if (cmdState == BASH_CMD_START && keywordEnds)
319 cmdStateNew = BASH_CMD_START;
320 else
321 sc.ChangeState(SCE_SH_IDENTIFIER);
323 // 'for'|'case'|'select' needs 'in'|'do' to be highlighted later
324 else if (bashStruct_in.InList(s)) {
325 if (cmdState == BASH_CMD_START && keywordEnds)
326 cmdStateNew = BASH_CMD_WORD;
327 else
328 sc.ChangeState(SCE_SH_IDENTIFIER);
330 // disambiguate option items and file test operators
331 else if (s[0] == '-') {
332 if (cmdState != BASH_CMD_TEST)
333 sc.ChangeState(SCE_SH_IDENTIFIER);
335 // disambiguate keywords and identifiers
336 else if (cmdState != BASH_CMD_START
337 || !(keywords.InList(s) && keywordEnds)) {
338 sc.ChangeState(SCE_SH_IDENTIFIER);
340 sc.SetState(SCE_SH_DEFAULT);
342 break;
343 case SCE_SH_IDENTIFIER:
344 if (sc.chPrev == '\\') { // for escaped chars
345 sc.ForwardSetState(SCE_SH_DEFAULT);
346 } else if (!setWord.Contains(sc.ch)) {
347 sc.SetState(SCE_SH_DEFAULT);
349 break;
350 case SCE_SH_NUMBER:
351 digit = translateBashDigit(sc.ch);
352 if (numBase == BASH_BASE_DECIMAL) {
353 if (sc.ch == '#') {
354 char s[10];
355 sc.GetCurrent(s, sizeof(s));
356 numBase = getBashNumberBase(s);
357 if (numBase != BASH_BASE_ERROR)
358 break;
359 } else if (IsADigit(sc.ch))
360 break;
361 } else if (numBase == BASH_BASE_HEX) {
362 if (IsADigit(sc.ch, 16))
363 break;
364 #ifdef PEDANTIC_OCTAL
365 } else if (numBase == BASH_BASE_OCTAL ||
366 numBase == BASH_BASE_OCTAL_ERROR) {
367 if (digit <= 7)
368 break;
369 if (digit <= 9) {
370 numBase = BASH_BASE_OCTAL_ERROR;
371 break;
373 #endif
374 } else if (numBase == BASH_BASE_ERROR) {
375 if (digit <= 9)
376 break;
377 } else { // DD#DDDD number style handling
378 if (digit != BASH_BASE_ERROR) {
379 if (numBase <= 36) {
380 // case-insensitive if base<=36
381 if (digit >= 36) digit -= 26;
383 if (digit < numBase)
384 break;
385 if (digit <= 9) {
386 numBase = BASH_BASE_ERROR;
387 break;
391 // fallthrough when number is at an end or error
392 if (numBase == BASH_BASE_ERROR
393 #ifdef PEDANTIC_OCTAL
394 || numBase == BASH_BASE_OCTAL_ERROR
395 #endif
397 sc.ChangeState(SCE_SH_ERROR);
399 sc.SetState(SCE_SH_DEFAULT);
400 break;
401 case SCE_SH_COMMENTLINE:
402 if (sc.atLineEnd && sc.chPrev != '\\') {
403 sc.SetState(SCE_SH_DEFAULT);
405 break;
406 case SCE_SH_HERE_DELIM:
407 // From Bash info:
408 // ---------------
409 // Specifier format is: <<[-]WORD
410 // Optional '-' is for removal of leading tabs from here-doc.
411 // Whitespace acceptable after <<[-] operator
413 if (HereDoc.State == 0) { // '<<' encountered
414 HereDoc.Quote = sc.chNext;
415 HereDoc.Quoted = false;
416 HereDoc.DelimiterLength = 0;
417 HereDoc.Delimiter[HereDoc.DelimiterLength] = '\0';
418 if (sc.chNext == '\'' || sc.chNext == '\"') { // a quoted here-doc delimiter (' or ")
419 sc.Forward();
420 HereDoc.Quoted = true;
421 HereDoc.State = 1;
422 } else if (setHereDoc.Contains(sc.chNext)) {
423 // an unquoted here-doc delimiter, no special handling
424 // TODO check what exactly bash considers part of the delim
425 HereDoc.State = 1;
426 } else if (sc.chNext == '<') { // HERE string <<<
427 sc.Forward();
428 sc.ForwardSetState(SCE_SH_DEFAULT);
429 } else if (IsASpace(sc.chNext)) {
430 // eat whitespace
431 } else if (setLeftShift.Contains(sc.chNext)) {
432 // left shift << or <<= operator cases
433 sc.ChangeState(SCE_SH_OPERATOR);
434 sc.ForwardSetState(SCE_SH_DEFAULT);
435 } else {
436 // symbols terminates; deprecated zero-length delimiter
437 HereDoc.State = 1;
439 } else if (HereDoc.State == 1) { // collect the delimiter
440 // * if single quoted, there's no escape
441 // * if double quoted, there are \\ and \" escapes
442 if ((HereDoc.Quote == '\'' && sc.ch != HereDoc.Quote) ||
443 (HereDoc.Quoted && sc.ch != HereDoc.Quote && sc.ch != '\\') ||
444 (HereDoc.Quote != '\'' && sc.chPrev == '\\') ||
445 (setHereDoc2.Contains(sc.ch))) {
446 HereDoc.Append(sc.ch);
447 } else if (HereDoc.Quoted && sc.ch == HereDoc.Quote) { // closing quote => end of delimiter
448 sc.ForwardSetState(SCE_SH_DEFAULT);
449 } else if (sc.ch == '\\') {
450 if (HereDoc.Quoted && sc.chNext != HereDoc.Quote && sc.chNext != '\\') {
451 // in quoted prefixes only \ and the quote eat the escape
452 HereDoc.Append(sc.ch);
453 } else {
454 // skip escape prefix
456 } else if (!HereDoc.Quoted) {
457 sc.SetState(SCE_SH_DEFAULT);
459 if (HereDoc.DelimiterLength >= HERE_DELIM_MAX - 1) { // force blowup
460 sc.SetState(SCE_SH_ERROR);
461 HereDoc.State = 0;
464 break;
465 case SCE_SH_HERE_Q:
466 // HereDoc.State == 2
467 if (sc.atLineStart) {
468 sc.SetState(SCE_SH_HERE_Q);
469 int prefixws = 0;
470 while (sc.ch == '\t' && !sc.atLineEnd) { // tabulation prefix
471 sc.Forward();
472 prefixws++;
474 if (prefixws > 0)
475 sc.SetState(SCE_SH_HERE_Q);
476 while (!sc.atLineEnd) {
477 sc.Forward();
479 char s[HERE_DELIM_MAX];
480 sc.GetCurrent(s, sizeof(s));
481 if (sc.LengthCurrent() == 0) { // '' or "" delimiters
482 if ((prefixws == 0 || HereDoc.Indent) &&
483 HereDoc.Quoted && HereDoc.DelimiterLength == 0)
484 sc.SetState(SCE_SH_DEFAULT);
485 break;
487 if (s[strlen(s) - 1] == '\r')
488 s[strlen(s) - 1] = '\0';
489 if (strcmp(HereDoc.Delimiter, s) == 0) {
490 if ((prefixws == 0) || // indentation rule
491 (prefixws > 0 && HereDoc.Indent)) {
492 sc.SetState(SCE_SH_DEFAULT);
493 break;
497 break;
498 case SCE_SH_SCALAR: // variable names
499 if (!setParam.Contains(sc.ch)) {
500 if (sc.LengthCurrent() == 1) {
501 // Special variable: $(, $_ etc.
502 sc.ForwardSetState(SCE_SH_DEFAULT);
503 } else {
504 sc.SetState(SCE_SH_DEFAULT);
507 break;
508 case SCE_SH_STRING: // delimited styles, can nest
509 case SCE_SH_BACKTICKS:
510 if (sc.ch == '\\' && QuoteStack.Up != '\\') {
511 if (QuoteStack.Style != BASH_DELIM_LITERAL)
512 sc.Forward();
513 } else if (sc.ch == QuoteStack.Down) {
514 QuoteStack.Count--;
515 if (QuoteStack.Count == 0) {
516 if (QuoteStack.Depth > 0) {
517 QuoteStack.Pop();
518 } else
519 sc.ForwardSetState(SCE_SH_DEFAULT);
521 } else if (sc.ch == QuoteStack.Up) {
522 QuoteStack.Count++;
523 } else {
524 if (QuoteStack.Style == BASH_DELIM_STRING ||
525 QuoteStack.Style == BASH_DELIM_LSTRING
526 ) { // do nesting for "string", $"locale-string"
527 if (sc.ch == '`') {
528 QuoteStack.Push(sc.ch, BASH_DELIM_BACKTICK);
529 } else if (sc.ch == '$' && sc.chNext == '(') {
530 sc.Forward();
531 QuoteStack.Push(sc.ch, BASH_DELIM_COMMAND);
533 } else if (QuoteStack.Style == BASH_DELIM_COMMAND ||
534 QuoteStack.Style == BASH_DELIM_BACKTICK
535 ) { // do nesting for $(command), `command`
536 if (sc.ch == '\'') {
537 QuoteStack.Push(sc.ch, BASH_DELIM_LITERAL);
538 } else if (sc.ch == '\"') {
539 QuoteStack.Push(sc.ch, BASH_DELIM_STRING);
540 } else if (sc.ch == '`') {
541 QuoteStack.Push(sc.ch, BASH_DELIM_BACKTICK);
542 } else if (sc.ch == '$') {
543 if (sc.chNext == '\'') {
544 sc.Forward();
545 QuoteStack.Push(sc.ch, BASH_DELIM_CSTRING);
546 } else if (sc.chNext == '\"') {
547 sc.Forward();
548 QuoteStack.Push(sc.ch, BASH_DELIM_LSTRING);
549 } else if (sc.chNext == '(') {
550 sc.Forward();
551 QuoteStack.Push(sc.ch, BASH_DELIM_COMMAND);
556 break;
557 case SCE_SH_PARAM: // ${parameter}
558 if (sc.ch == '\\' && Quote.Up != '\\') {
559 sc.Forward();
560 } else if (sc.ch == Quote.Down) {
561 Quote.Count--;
562 if (Quote.Count == 0) {
563 sc.ForwardSetState(SCE_SH_DEFAULT);
565 } else if (sc.ch == Quote.Up) {
566 Quote.Count++;
568 break;
569 case SCE_SH_CHARACTER: // singly-quoted strings
570 if (sc.ch == Quote.Down) {
571 Quote.Count--;
572 if (Quote.Count == 0) {
573 sc.ForwardSetState(SCE_SH_DEFAULT);
576 break;
579 // Must check end of HereDoc state 1 before default state is handled
580 if (HereDoc.State == 1 && sc.atLineEnd) {
581 // Begin of here-doc (the line after the here-doc delimiter):
582 // Lexically, the here-doc starts from the next line after the >>, but the
583 // first line of here-doc seem to follow the style of the last EOL sequence
584 HereDoc.State = 2;
585 if (HereDoc.Quoted) {
586 if (sc.state == SCE_SH_HERE_DELIM) {
587 // Missing quote at end of string! We are stricter than bash.
588 // Colour here-doc anyway while marking this bit as an error.
589 sc.ChangeState(SCE_SH_ERROR);
591 // HereDoc.Quote always == '\''
592 sc.SetState(SCE_SH_HERE_Q);
593 } else if (HereDoc.DelimiterLength == 0) {
594 // no delimiter, illegal (but '' and "" are legal)
595 sc.ChangeState(SCE_SH_ERROR);
596 sc.SetState(SCE_SH_DEFAULT);
597 } else {
598 sc.SetState(SCE_SH_HERE_Q);
602 // update cmdState about the current command segment
603 if (stylePrev != SCE_SH_DEFAULT && sc.state == SCE_SH_DEFAULT) {
604 cmdState = cmdStateNew;
606 // Determine if a new state should be entered.
607 if (sc.state == SCE_SH_DEFAULT) {
608 if (sc.ch == '\\') {
609 // Bash can escape any non-newline as a literal
610 sc.SetState(SCE_SH_IDENTIFIER);
611 if (sc.chNext == '\r' || sc.chNext == '\n')
612 sc.SetState(SCE_SH_OPERATOR);
613 } else if (IsADigit(sc.ch)) {
614 sc.SetState(SCE_SH_NUMBER);
615 numBase = BASH_BASE_DECIMAL;
616 if (sc.ch == '0') { // hex,octal
617 if (sc.chNext == 'x' || sc.chNext == 'X') {
618 numBase = BASH_BASE_HEX;
619 sc.Forward();
620 } else if (IsADigit(sc.chNext)) {
621 #ifdef PEDANTIC_OCTAL
622 numBase = BASH_BASE_OCTAL;
623 #else
624 numBase = BASH_BASE_HEX;
625 #endif
628 } else if (setWordStart.Contains(sc.ch)) {
629 sc.SetState(SCE_SH_WORD);
630 } else if (sc.ch == '#') {
631 if (stylePrev != SCE_SH_WORD && stylePrev != SCE_SH_IDENTIFIER &&
632 (sc.currentPos == 0 || setMetaCharacter.Contains(sc.chPrev))) {
633 sc.SetState(SCE_SH_COMMENTLINE);
634 } else {
635 sc.SetState(SCE_SH_WORD);
637 } else if (sc.ch == '\"') {
638 sc.SetState(SCE_SH_STRING);
639 QuoteStack.Start(sc.ch, BASH_DELIM_STRING);
640 } else if (sc.ch == '\'') {
641 sc.SetState(SCE_SH_CHARACTER);
642 Quote.Start(sc.ch);
643 } else if (sc.ch == '`') {
644 sc.SetState(SCE_SH_BACKTICKS);
645 QuoteStack.Start(sc.ch, BASH_DELIM_BACKTICK);
646 } else if (sc.ch == '$') {
647 if (sc.Match("$((")) {
648 sc.SetState(SCE_SH_OPERATOR); // handle '((' later
649 continue;
651 sc.SetState(SCE_SH_SCALAR);
652 sc.Forward();
653 if (sc.ch == '{') {
654 sc.ChangeState(SCE_SH_PARAM);
655 Quote.Start(sc.ch);
656 } else if (sc.ch == '\'') {
657 sc.ChangeState(SCE_SH_STRING);
658 QuoteStack.Start(sc.ch, BASH_DELIM_CSTRING);
659 } else if (sc.ch == '"') {
660 sc.ChangeState(SCE_SH_STRING);
661 QuoteStack.Start(sc.ch, BASH_DELIM_LSTRING);
662 } else if (sc.ch == '(') {
663 sc.ChangeState(SCE_SH_BACKTICKS);
664 QuoteStack.Start(sc.ch, BASH_DELIM_COMMAND);
665 } else if (sc.ch == '`') { // $` seen in a configure script, valid?
666 sc.ChangeState(SCE_SH_BACKTICKS);
667 QuoteStack.Start(sc.ch, BASH_DELIM_BACKTICK);
668 } else {
669 continue; // scalar has no delimiter pair
671 } else if (sc.Match('<', '<')) {
672 sc.SetState(SCE_SH_HERE_DELIM);
673 HereDoc.State = 0;
674 if (sc.GetRelative(2) == '-') { // <<- indent case
675 HereDoc.Indent = true;
676 sc.Forward();
677 } else {
678 HereDoc.Indent = false;
680 } else if (sc.ch == '-' && // one-char file test operators
681 setSingleCharOp.Contains(sc.chNext) &&
682 !setWord.Contains(sc.GetRelative(2)) &&
683 IsASpace(sc.chPrev)) {
684 sc.SetState(SCE_SH_WORD);
685 sc.Forward();
686 } else if (setBashOperator.Contains(sc.ch)) {
687 char s[10];
688 bool isCmdDelim = false;
689 sc.SetState(SCE_SH_OPERATOR);
690 // handle opening delimiters for test/arithmetic expressions - ((,[[,[
691 if (cmdState == BASH_CMD_START
692 || cmdState == BASH_CMD_BODY) {
693 if (sc.Match('(', '(')) {
694 cmdState = BASH_CMD_ARITH;
695 sc.Forward();
696 } else if (sc.Match('[', '[') && IsASpace(sc.GetRelative(2))) {
697 cmdState = BASH_CMD_TEST;
698 testExprType = 1;
699 sc.Forward();
700 } else if (sc.ch == '[' && IsASpace(sc.chNext)) {
701 cmdState = BASH_CMD_TEST;
702 testExprType = 2;
705 // special state -- for ((x;y;z)) in ... looping
706 if (cmdState == BASH_CMD_WORD && sc.Match('(', '(')) {
707 cmdState = BASH_CMD_ARITH;
708 sc.Forward();
709 continue;
711 // handle command delimiters in command START|BODY|WORD state, also TEST if 'test'
712 if (cmdState == BASH_CMD_START
713 || cmdState == BASH_CMD_BODY
714 || cmdState == BASH_CMD_WORD
715 || (cmdState == BASH_CMD_TEST && testExprType == 0)) {
716 s[0] = static_cast<char>(sc.ch);
717 if (setBashOperator.Contains(sc.chNext)) {
718 s[1] = static_cast<char>(sc.chNext);
719 s[2] = '\0';
720 isCmdDelim = cmdDelimiter.InList(s);
721 if (isCmdDelim)
722 sc.Forward();
724 if (!isCmdDelim) {
725 s[1] = '\0';
726 isCmdDelim = cmdDelimiter.InList(s);
728 if (isCmdDelim) {
729 cmdState = BASH_CMD_DELIM;
730 continue;
733 // handle closing delimiters for test/arithmetic expressions - )),]],]
734 if (cmdState == BASH_CMD_ARITH && sc.Match(')', ')')) {
735 cmdState = BASH_CMD_BODY;
736 sc.Forward();
737 } else if (cmdState == BASH_CMD_TEST && IsASpace(sc.chPrev)) {
738 if (sc.Match(']', ']') && testExprType == 1) {
739 sc.Forward();
740 cmdState = BASH_CMD_BODY;
741 } else if (sc.ch == ']' && testExprType == 2) {
742 cmdState = BASH_CMD_BODY;
746 }// sc.state
748 sc.Complete();
749 if (sc.state == SCE_SH_HERE_Q) {
750 styler.ChangeLexerState(sc.currentPos, styler.Length());
752 sc.Complete();
755 static bool IsCommentLine(int line, Accessor &styler) {
756 int pos = styler.LineStart(line);
757 int eol_pos = styler.LineStart(line + 1) - 1;
758 for (int i = pos; i < eol_pos; i++) {
759 char ch = styler[i];
760 if (ch == '#')
761 return true;
762 else if (ch != ' ' && ch != '\t')
763 return false;
765 return false;
768 static void FoldBashDoc(unsigned int startPos, int length, int, WordList *[],
769 Accessor &styler) {
770 bool foldComment = styler.GetPropertyInt("fold.comment") != 0;
771 bool foldCompact = styler.GetPropertyInt("fold.compact", 1) != 0;
772 unsigned int endPos = startPos + length;
773 int visibleChars = 0;
774 int skipHereCh = 0;
775 int lineCurrent = styler.GetLine(startPos);
776 int levelPrev = styler.LevelAt(lineCurrent) & SC_FOLDLEVELNUMBERMASK;
777 int levelCurrent = levelPrev;
778 char chNext = styler[startPos];
779 int styleNext = styler.StyleAt(startPos);
780 for (unsigned int i = startPos; i < endPos; i++) {
781 char ch = chNext;
782 chNext = styler.SafeGetCharAt(i + 1);
783 int style = styleNext;
784 styleNext = styler.StyleAt(i + 1);
785 bool atEOL = (ch == '\r' && chNext != '\n') || (ch == '\n');
786 // Comment folding
787 if (foldComment && atEOL && IsCommentLine(lineCurrent, styler))
789 if (!IsCommentLine(lineCurrent - 1, styler)
790 && IsCommentLine(lineCurrent + 1, styler))
791 levelCurrent++;
792 else if (IsCommentLine(lineCurrent - 1, styler)
793 && !IsCommentLine(lineCurrent + 1, styler))
794 levelCurrent--;
796 if (style == SCE_SH_OPERATOR) {
797 if (ch == '{') {
798 levelCurrent++;
799 } else if (ch == '}') {
800 levelCurrent--;
803 // Here Document folding
804 if (style == SCE_SH_HERE_DELIM) {
805 if (ch == '<' && chNext == '<') {
806 if (styler.SafeGetCharAt(i + 2) == '<') {
807 skipHereCh = 1;
808 } else {
809 if (skipHereCh == 0) {
810 levelCurrent++;
811 } else {
812 skipHereCh = 0;
816 } else if (style == SCE_SH_HERE_Q && styler.StyleAt(i+1) == SCE_SH_DEFAULT) {
817 levelCurrent--;
819 if (atEOL) {
820 int lev = levelPrev;
821 if (visibleChars == 0 && foldCompact)
822 lev |= SC_FOLDLEVELWHITEFLAG;
823 if ((levelCurrent > levelPrev) && (visibleChars > 0))
824 lev |= SC_FOLDLEVELHEADERFLAG;
825 if (lev != styler.LevelAt(lineCurrent)) {
826 styler.SetLevel(lineCurrent, lev);
828 lineCurrent++;
829 levelPrev = levelCurrent;
830 visibleChars = 0;
832 if (!isspacechar(ch))
833 visibleChars++;
835 // Fill in the real level of the next line, keeping the current flags as they will be filled in later
836 int flagsNext = styler.LevelAt(lineCurrent) & ~SC_FOLDLEVELNUMBERMASK;
837 styler.SetLevel(lineCurrent, levelPrev | flagsNext);
840 static const char * const bashWordListDesc[] = {
841 "Keywords",
845 LexerModule lmBash(SCLEX_BASH, ColouriseBashDoc, "bash", FoldBashDoc, bashWordListDesc);