1 // Scintilla source code edit control
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.
16 #include "Scintilla.h"
20 #include "LexAccessor.h"
22 #include "StyleContext.h"
23 #include "CharacterSet.h"
24 #include "LexerModule.h"
27 using namespace Scintilla
;
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
36 #define BASH_BASE_ERROR 65
37 #define BASH_BASE_DECIMAL 66
38 #define BASH_BASE_HEX 67
40 #define BASH_BASE_OCTAL 68
41 #define BASH_BASE_OCTAL_ERROR 69
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') {
66 } else if (ch
>= 'a' && ch
<= 'z') {
68 } else if (ch
>= 'A' && ch
<= 'Z') {
70 } else if (ch
== '@') {
72 } else if (ch
== '_') {
75 return BASH_BASE_ERROR
;
78 static inline int getBashNumberBase(char *s
) {
82 base
= base
* 10 + (*s
++ - '0');
85 if (base
> 64 || i
> 2) {
86 return BASH_BASE_ERROR
;
91 static int opposite(int ch
) {
92 if (ch
== '(') return ')';
93 if (ch
== '[') return ']';
94 if (ch
== '{') return '}';
95 if (ch
== '<') return '>';
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
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
136 Delimiter
= new char[HERE_DELIM_MAX
];
139 void Append(int ch
) {
140 Delimiter
[DelimiterLength
++] = static_cast<char>(ch
);
141 Delimiter
[DelimiterLength
] = '\0';
149 class QuoteCls
{ // Class to manage quote pairs (simplified vs LexPerl)
170 class QuoteStackCls
{ // Class to manage quote pairs that nest
175 int Depth
; // levels pushed
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
) {
195 void Push(int u
, int s
) {
196 if (Depth
>= BASH_DELIM_STACK_MAX
)
198 CountStack
[Depth
] = Count
;
199 UpStack
[Depth
] = Up
;
200 StyleStack
[Depth
] = Style
;
211 Count
= CountStack
[Depth
];
212 Up
= UpStack
[Depth
];
213 Style
= StyleStack
[Depth
];
222 QuoteStackCls QuoteStack
;
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
)))
236 startPos
= styler
.LineStart(ln
);
237 if (ln
== 0 || styler
.GetLineState(ln
) == BASH_CMD_START
)
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
);
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
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.
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
;
288 // "." never used in Bash variable names but used in file names
289 if (!setWord
.Contains(sc
.ch
)) {
292 sc
.GetCurrent(s
, sizeof(s
));
293 // allow keywords ending in a whitespace or command delimiter
294 s2
[0] = static_cast<char>(sc
.ch
);
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
;
304 sc
.ChangeState(SCE_SH_IDENTIFIER
);
305 sc
.SetState(SCE_SH_DEFAULT
);
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
;
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
;
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
;
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
);
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
);
351 digit
= translateBashDigit(sc
.ch
);
352 if (numBase
== BASH_BASE_DECIMAL
) {
355 sc
.GetCurrent(s
, sizeof(s
));
356 numBase
= getBashNumberBase(s
);
357 if (numBase
!= BASH_BASE_ERROR
)
359 } else if (IsADigit(sc
.ch
))
361 } else if (numBase
== BASH_BASE_HEX
) {
362 if (IsADigit(sc
.ch
, 16))
364 #ifdef PEDANTIC_OCTAL
365 } else if (numBase
== BASH_BASE_OCTAL
||
366 numBase
== BASH_BASE_OCTAL_ERROR
) {
370 numBase
= BASH_BASE_OCTAL_ERROR
;
374 } else if (numBase
== BASH_BASE_ERROR
) {
377 } else { // DD#DDDD number style handling
378 if (digit
!= BASH_BASE_ERROR
) {
380 // case-insensitive if base<=36
381 if (digit
>= 36) digit
-= 26;
386 numBase
= BASH_BASE_ERROR
;
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
397 sc
.ChangeState(SCE_SH_ERROR
);
399 sc
.SetState(SCE_SH_DEFAULT
);
401 case SCE_SH_COMMENTLINE
:
402 if (sc
.atLineEnd
&& sc
.chPrev
!= '\\') {
403 sc
.SetState(SCE_SH_DEFAULT
);
406 case SCE_SH_HERE_DELIM
:
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 ")
420 HereDoc
.Quoted
= true;
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
426 } else if (sc
.chNext
== '<') { // HERE string <<<
428 sc
.ForwardSetState(SCE_SH_DEFAULT
);
429 } else if (IsASpace(sc
.chNext
)) {
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
);
436 // symbols terminates; deprecated zero-length delimiter
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
);
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
);
466 // HereDoc.State == 2
467 if (sc
.atLineStart
) {
468 sc
.SetState(SCE_SH_HERE_Q
);
470 while (sc
.ch
== '\t' && !sc
.atLineEnd
) { // tabulation prefix
475 sc
.SetState(SCE_SH_HERE_Q
);
476 while (!sc
.atLineEnd
) {
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
);
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
);
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
);
504 sc
.SetState(SCE_SH_DEFAULT
);
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
)
513 } else if (sc
.ch
== QuoteStack
.Down
) {
515 if (QuoteStack
.Count
== 0) {
516 if (QuoteStack
.Depth
> 0) {
519 sc
.ForwardSetState(SCE_SH_DEFAULT
);
521 } else if (sc
.ch
== QuoteStack
.Up
) {
524 if (QuoteStack
.Style
== BASH_DELIM_STRING
||
525 QuoteStack
.Style
== BASH_DELIM_LSTRING
526 ) { // do nesting for "string", $"locale-string"
528 QuoteStack
.Push(sc
.ch
, BASH_DELIM_BACKTICK
);
529 } else if (sc
.ch
== '$' && sc
.chNext
== '(') {
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`
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
== '\'') {
545 QuoteStack
.Push(sc
.ch
, BASH_DELIM_CSTRING
);
546 } else if (sc
.chNext
== '\"') {
548 QuoteStack
.Push(sc
.ch
, BASH_DELIM_LSTRING
);
549 } else if (sc
.chNext
== '(') {
551 QuoteStack
.Push(sc
.ch
, BASH_DELIM_COMMAND
);
557 case SCE_SH_PARAM
: // ${parameter}
558 if (sc
.ch
== '\\' && Quote
.Up
!= '\\') {
560 } else if (sc
.ch
== Quote
.Down
) {
562 if (Quote
.Count
== 0) {
563 sc
.ForwardSetState(SCE_SH_DEFAULT
);
565 } else if (sc
.ch
== Quote
.Up
) {
569 case SCE_SH_CHARACTER
: // singly-quoted strings
570 if (sc
.ch
== Quote
.Down
) {
572 if (Quote
.Count
== 0) {
573 sc
.ForwardSetState(SCE_SH_DEFAULT
);
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
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
);
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
) {
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
;
620 } else if (IsADigit(sc
.chNext
)) {
621 #ifdef PEDANTIC_OCTAL
622 numBase
= BASH_BASE_OCTAL
;
624 numBase
= BASH_BASE_HEX
;
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
);
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
);
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
651 sc
.SetState(SCE_SH_SCALAR
);
654 sc
.ChangeState(SCE_SH_PARAM
);
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
);
669 continue; // scalar has no delimiter pair
671 } else if (sc
.Match('<', '<')) {
672 sc
.SetState(SCE_SH_HERE_DELIM
);
674 if (sc
.GetRelative(2) == '-') { // <<- indent case
675 HereDoc
.Indent
= true;
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
);
686 } else if (setBashOperator
.Contains(sc
.ch
)) {
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
;
696 } else if (sc
.Match('[', '[') && IsASpace(sc
.GetRelative(2))) {
697 cmdState
= BASH_CMD_TEST
;
700 } else if (sc
.ch
== '[' && IsASpace(sc
.chNext
)) {
701 cmdState
= BASH_CMD_TEST
;
705 // special state -- for ((x;y;z)) in ... looping
706 if (cmdState
== BASH_CMD_WORD
&& sc
.Match('(', '(')) {
707 cmdState
= BASH_CMD_ARITH
;
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
);
720 isCmdDelim
= cmdDelimiter
.InList(s
);
726 isCmdDelim
= cmdDelimiter
.InList(s
);
729 cmdState
= BASH_CMD_DELIM
;
733 // handle closing delimiters for test/arithmetic expressions - )),]],]
734 if (cmdState
== BASH_CMD_ARITH
&& sc
.Match(')', ')')) {
735 cmdState
= BASH_CMD_BODY
;
737 } else if (cmdState
== BASH_CMD_TEST
&& IsASpace(sc
.chPrev
)) {
738 if (sc
.Match(']', ']') && testExprType
== 1) {
740 cmdState
= BASH_CMD_BODY
;
741 } else if (sc
.ch
== ']' && testExprType
== 2) {
742 cmdState
= BASH_CMD_BODY
;
749 if (sc
.state
== SCE_SH_HERE_Q
) {
750 styler
.ChangeLexerState(sc
.currentPos
, styler
.Length());
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
++) {
762 else if (ch
!= ' ' && ch
!= '\t')
768 static void FoldBashDoc(unsigned int startPos
, int length
, int, WordList
*[],
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;
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
++) {
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');
787 if (foldComment
&& atEOL
&& IsCommentLine(lineCurrent
, styler
))
789 if (!IsCommentLine(lineCurrent
- 1, styler
)
790 && IsCommentLine(lineCurrent
+ 1, styler
))
792 else if (IsCommentLine(lineCurrent
- 1, styler
)
793 && !IsCommentLine(lineCurrent
+ 1, styler
))
796 if (style
== SCE_SH_OPERATOR
) {
799 } else if (ch
== '}') {
803 // Here Document folding
804 if (style
== SCE_SH_HERE_DELIM
) {
805 if (ch
== '<' && chNext
== '<') {
806 if (styler
.SafeGetCharAt(i
+ 2) == '<') {
809 if (skipHereCh
== 0) {
816 } else if (style
== SCE_SH_HERE_Q
&& styler
.StyleAt(i
+1) == SCE_SH_DEFAULT
) {
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
);
829 levelPrev
= levelCurrent
;
832 if (!isspacechar(ch
))
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
[] = {
845 LexerModule
lmBash(SCLEX_BASH
, ColouriseBashDoc
, "bash", FoldBashDoc
, bashWordListDesc
);