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 int GlobScan(StyleContext
&sc
) {
100 // forward scan for zsh globs, disambiguate versus bash arrays
101 // complex expressions may still fail, e.g. unbalanced () '' "" etc
105 while ((c
= sc
.GetRelativeCharacter(++sLen
)) != 0) {
108 } else if (c
== '\'' || c
== '\"') {
109 if (hash
!= 2) return 0;
110 } else if (c
== '#' && hash
== 0) {
111 hash
= (sLen
== 1) ? 2:1;
112 } else if (c
== '(') {
114 } else if (c
== ')') {
116 if (hash
) return sLen
;
125 static void ColouriseBashDoc(Sci_PositionU startPos
, Sci_Position length
, int initStyle
,
126 WordList
*keywordlists
[], Accessor
&styler
) {
128 WordList
&keywords
= *keywordlists
[0];
129 WordList cmdDelimiter
, bashStruct
, bashStruct_in
;
130 cmdDelimiter
.Set("| || |& & && ; ;; ( ) { }");
131 bashStruct
.Set("if elif fi while until else then do done esac eval");
132 bashStruct_in
.Set("for case select");
134 CharacterSet
setWordStart(CharacterSet::setAlpha
, "_");
135 // note that [+-] are often parts of identifiers in shell scripts
136 CharacterSet
setWord(CharacterSet::setAlphaNum
, "._+-");
137 CharacterSet
setMetaCharacter(CharacterSet::setNone
, "|&;()<> \t\r\n");
138 setMetaCharacter
.Add(0);
139 CharacterSet
setBashOperator(CharacterSet::setNone
, "^&%()-+=|{}[]:;>,*/<?!.~@");
140 CharacterSet
setSingleCharOp(CharacterSet::setNone
, "rwxoRWXOezsfdlpSbctugkTBMACahGLNn");
141 CharacterSet
setParam(CharacterSet::setAlphaNum
, "$_");
142 CharacterSet
setHereDoc(CharacterSet::setAlpha
, "_\\-+!%*,./:?@[]^`{}~");
143 CharacterSet
setHereDoc2(CharacterSet::setAlphaNum
, "_-+!%*,./:=?@[]^`{}~");
144 CharacterSet
setLeftShift(CharacterSet::setDigits
, "$");
146 class HereDocCls
{ // Class to manage HERE document elements
148 int State
; // 0: '<<' encountered
149 // 1: collect the delimiter
150 // 2: here doc text (lines after the delimiter)
151 int Quote
; // the char after '<<'
152 bool Quoted
; // true if Quote in ('\'','"','`')
153 bool Indent
; // indented delimiter (for <<-)
154 int DelimiterLength
; // strlen(Delimiter)
155 char Delimiter
[HERE_DELIM_MAX
]; // the Delimiter
164 void Append(int ch
) {
165 Delimiter
[DelimiterLength
++] = static_cast<char>(ch
);
166 Delimiter
[DelimiterLength
] = '\0';
173 class QuoteCls
{ // Class to manage quote pairs (simplified vs LexPerl)
194 class QuoteStackCls
{ // Class to manage quote pairs that nest
199 int Depth
; // levels pushed
200 int CountStack
[BASH_DELIM_STACK_MAX
];
201 int UpStack
[BASH_DELIM_STACK_MAX
];
202 int StyleStack
[BASH_DELIM_STACK_MAX
];
210 void Start(int u
, int s
) {
216 void Push(int u
, int s
) {
217 if (Depth
>= BASH_DELIM_STACK_MAX
)
219 CountStack
[Depth
] = Count
;
220 UpStack
[Depth
] = Up
;
221 StyleStack
[Depth
] = Style
;
232 Count
= CountStack
[Depth
];
233 Up
= UpStack
[Depth
];
234 Style
= StyleStack
[Depth
];
240 QuoteStackCls QuoteStack
;
244 Sci_PositionU endPos
= startPos
+ length
;
245 int cmdState
= BASH_CMD_START
;
246 int testExprType
= 0;
248 // Always backtracks to the start of a line that is not a continuation
249 // of the previous line (i.e. start of a bash command segment)
250 Sci_Position ln
= styler
.GetLine(startPos
);
251 if (ln
> 0 && startPos
== static_cast<Sci_PositionU
>(styler
.LineStart(ln
)))
254 startPos
= styler
.LineStart(ln
);
255 if (ln
== 0 || styler
.GetLineState(ln
) == BASH_CMD_START
)
259 initStyle
= SCE_SH_DEFAULT
;
261 StyleContext
sc(startPos
, endPos
- startPos
, initStyle
, styler
);
263 for (; sc
.More(); sc
.Forward()) {
265 // handle line continuation, updates per-line stored state
266 if (sc
.atLineStart
) {
267 ln
= styler
.GetLine(sc
.currentPos
);
268 if (sc
.state
== SCE_SH_STRING
269 || sc
.state
== SCE_SH_BACKTICKS
270 || sc
.state
== SCE_SH_CHARACTER
271 || sc
.state
== SCE_SH_HERE_Q
272 || sc
.state
== SCE_SH_COMMENTLINE
273 || sc
.state
== SCE_SH_PARAM
) {
274 // force backtrack while retaining cmdState
275 styler
.SetLineState(ln
, BASH_CMD_BODY
);
278 if ((sc
.GetRelative(-3) == '\\' && sc
.GetRelative(-2) == '\r' && sc
.chPrev
== '\n')
279 || sc
.GetRelative(-2) == '\\') { // handle '\' line continuation
280 // retain last line's state
282 cmdState
= BASH_CMD_START
;
284 styler
.SetLineState(ln
, cmdState
);
288 // controls change of cmdState at the end of a non-whitespace element
289 // states BODY|TEST|ARITH persist until the end of a command segment
290 // state WORD persist, but ends with 'in' or 'do' construct keywords
291 int cmdStateNew
= BASH_CMD_BODY
;
292 if (cmdState
== BASH_CMD_TEST
|| cmdState
== BASH_CMD_ARITH
|| cmdState
== BASH_CMD_WORD
)
293 cmdStateNew
= cmdState
;
294 int stylePrev
= sc
.state
;
296 // Determine if the current state should terminate.
298 case SCE_SH_OPERATOR
:
299 sc
.SetState(SCE_SH_DEFAULT
);
300 if (cmdState
== BASH_CMD_DELIM
) // if command delimiter, start new command
301 cmdStateNew
= BASH_CMD_START
;
302 else if (sc
.chPrev
== '\\') // propagate command state if line continued
303 cmdStateNew
= cmdState
;
306 // "." never used in Bash variable names but used in file names
307 if (!setWord
.Contains(sc
.ch
)) {
310 sc
.GetCurrent(s
, sizeof(s
));
311 // allow keywords ending in a whitespace or command delimiter
312 s2
[0] = static_cast<char>(sc
.ch
);
314 bool keywordEnds
= IsASpace(sc
.ch
) || cmdDelimiter
.InList(s2
);
315 // 'in' or 'do' may be construct keywords
316 if (cmdState
== BASH_CMD_WORD
) {
317 if (strcmp(s
, "in") == 0 && keywordEnds
)
318 cmdStateNew
= BASH_CMD_BODY
;
319 else if (strcmp(s
, "do") == 0 && keywordEnds
)
320 cmdStateNew
= BASH_CMD_START
;
322 sc
.ChangeState(SCE_SH_IDENTIFIER
);
323 sc
.SetState(SCE_SH_DEFAULT
);
326 // a 'test' keyword starts a test expression
327 if (strcmp(s
, "test") == 0) {
328 if (cmdState
== BASH_CMD_START
&& keywordEnds
) {
329 cmdStateNew
= BASH_CMD_TEST
;
332 sc
.ChangeState(SCE_SH_IDENTIFIER
);
334 // detect bash construct keywords
335 else if (bashStruct
.InList(s
)) {
336 if (cmdState
== BASH_CMD_START
&& keywordEnds
)
337 cmdStateNew
= BASH_CMD_START
;
339 sc
.ChangeState(SCE_SH_IDENTIFIER
);
341 // 'for'|'case'|'select' needs 'in'|'do' to be highlighted later
342 else if (bashStruct_in
.InList(s
)) {
343 if (cmdState
== BASH_CMD_START
&& keywordEnds
)
344 cmdStateNew
= BASH_CMD_WORD
;
346 sc
.ChangeState(SCE_SH_IDENTIFIER
);
348 // disambiguate option items and file test operators
349 else if (s
[0] == '-') {
350 if (cmdState
!= BASH_CMD_TEST
)
351 sc
.ChangeState(SCE_SH_IDENTIFIER
);
353 // disambiguate keywords and identifiers
354 else if (cmdState
!= BASH_CMD_START
355 || !(keywords
.InList(s
) && keywordEnds
)) {
356 sc
.ChangeState(SCE_SH_IDENTIFIER
);
358 sc
.SetState(SCE_SH_DEFAULT
);
361 case SCE_SH_IDENTIFIER
:
362 if (sc
.chPrev
== '\\') { // for escaped chars
363 sc
.ForwardSetState(SCE_SH_DEFAULT
);
364 } else if (!setWord
.Contains(sc
.ch
)) {
365 sc
.SetState(SCE_SH_DEFAULT
);
366 } else if (cmdState
== BASH_CMD_ARITH
&& !setWordStart
.Contains(sc
.ch
)) {
367 sc
.SetState(SCE_SH_DEFAULT
);
371 digit
= translateBashDigit(sc
.ch
);
372 if (numBase
== BASH_BASE_DECIMAL
) {
375 sc
.GetCurrent(s
, sizeof(s
));
376 numBase
= getBashNumberBase(s
);
377 if (numBase
!= BASH_BASE_ERROR
)
379 } else if (IsADigit(sc
.ch
))
381 } else if (numBase
== BASH_BASE_HEX
) {
382 if (IsADigit(sc
.ch
, 16))
384 #ifdef PEDANTIC_OCTAL
385 } else if (numBase
== BASH_BASE_OCTAL
||
386 numBase
== BASH_BASE_OCTAL_ERROR
) {
390 numBase
= BASH_BASE_OCTAL_ERROR
;
394 } else if (numBase
== BASH_BASE_ERROR
) {
397 } else { // DD#DDDD number style handling
398 if (digit
!= BASH_BASE_ERROR
) {
400 // case-insensitive if base<=36
401 if (digit
>= 36) digit
-= 26;
406 numBase
= BASH_BASE_ERROR
;
411 // fallthrough when number is at an end or error
412 if (numBase
== BASH_BASE_ERROR
413 #ifdef PEDANTIC_OCTAL
414 || numBase
== BASH_BASE_OCTAL_ERROR
417 sc
.ChangeState(SCE_SH_ERROR
);
419 sc
.SetState(SCE_SH_DEFAULT
);
421 case SCE_SH_COMMENTLINE
:
422 if (sc
.atLineEnd
&& sc
.chPrev
!= '\\') {
423 sc
.SetState(SCE_SH_DEFAULT
);
426 case SCE_SH_HERE_DELIM
:
429 // Specifier format is: <<[-]WORD
430 // Optional '-' is for removal of leading tabs from here-doc.
431 // Whitespace acceptable after <<[-] operator
433 if (HereDoc
.State
== 0) { // '<<' encountered
434 HereDoc
.Quote
= sc
.chNext
;
435 HereDoc
.Quoted
= false;
436 HereDoc
.DelimiterLength
= 0;
437 HereDoc
.Delimiter
[HereDoc
.DelimiterLength
] = '\0';
438 if (sc
.chNext
== '\'' || sc
.chNext
== '\"') { // a quoted here-doc delimiter (' or ")
440 HereDoc
.Quoted
= true;
442 } else if (setHereDoc
.Contains(sc
.chNext
) ||
443 (sc
.chNext
== '=' && cmdState
!= BASH_CMD_ARITH
)) {
444 // an unquoted here-doc delimiter, no special handling
446 } else if (sc
.chNext
== '<') { // HERE string <<<
448 sc
.ForwardSetState(SCE_SH_DEFAULT
);
449 } else if (IsASpace(sc
.chNext
)) {
451 } else if (setLeftShift
.Contains(sc
.chNext
) ||
452 (sc
.chNext
== '=' && cmdState
== BASH_CMD_ARITH
)) {
453 // left shift <<$var or <<= cases
454 sc
.ChangeState(SCE_SH_OPERATOR
);
455 sc
.ForwardSetState(SCE_SH_DEFAULT
);
457 // symbols terminates; deprecated zero-length delimiter
460 } else if (HereDoc
.State
== 1) { // collect the delimiter
461 // * if single quoted, there's no escape
462 // * if double quoted, there are \\ and \" escapes
463 if ((HereDoc
.Quote
== '\'' && sc
.ch
!= HereDoc
.Quote
) ||
464 (HereDoc
.Quoted
&& sc
.ch
!= HereDoc
.Quote
&& sc
.ch
!= '\\') ||
465 (HereDoc
.Quote
!= '\'' && sc
.chPrev
== '\\') ||
466 (setHereDoc2
.Contains(sc
.ch
))) {
467 HereDoc
.Append(sc
.ch
);
468 } else if (HereDoc
.Quoted
&& sc
.ch
== HereDoc
.Quote
) { // closing quote => end of delimiter
469 sc
.ForwardSetState(SCE_SH_DEFAULT
);
470 } else if (sc
.ch
== '\\') {
471 if (HereDoc
.Quoted
&& sc
.chNext
!= HereDoc
.Quote
&& sc
.chNext
!= '\\') {
472 // in quoted prefixes only \ and the quote eat the escape
473 HereDoc
.Append(sc
.ch
);
475 // skip escape prefix
477 } else if (!HereDoc
.Quoted
) {
478 sc
.SetState(SCE_SH_DEFAULT
);
480 if (HereDoc
.DelimiterLength
>= HERE_DELIM_MAX
- 1) { // force blowup
481 sc
.SetState(SCE_SH_ERROR
);
487 // HereDoc.State == 2
488 if (sc
.atLineStart
) {
489 sc
.SetState(SCE_SH_HERE_Q
);
491 while (sc
.ch
== '\t' && !sc
.atLineEnd
) { // tabulation prefix
496 sc
.SetState(SCE_SH_HERE_Q
);
497 while (!sc
.atLineEnd
) {
500 char s
[HERE_DELIM_MAX
];
501 sc
.GetCurrent(s
, sizeof(s
));
502 if (sc
.LengthCurrent() == 0) { // '' or "" delimiters
503 if ((prefixws
== 0 || HereDoc
.Indent
) &&
504 HereDoc
.Quoted
&& HereDoc
.DelimiterLength
== 0)
505 sc
.SetState(SCE_SH_DEFAULT
);
508 if (s
[strlen(s
) - 1] == '\r')
509 s
[strlen(s
) - 1] = '\0';
510 if (strcmp(HereDoc
.Delimiter
, s
) == 0) {
511 if ((prefixws
== 0) || // indentation rule
512 (prefixws
> 0 && HereDoc
.Indent
)) {
513 sc
.SetState(SCE_SH_DEFAULT
);
519 case SCE_SH_SCALAR
: // variable names
520 if (!setParam
.Contains(sc
.ch
)) {
521 if (sc
.LengthCurrent() == 1) {
522 // Special variable: $(, $_ etc.
523 sc
.ForwardSetState(SCE_SH_DEFAULT
);
525 sc
.SetState(SCE_SH_DEFAULT
);
529 case SCE_SH_STRING
: // delimited styles, can nest
530 case SCE_SH_BACKTICKS
:
531 if (sc
.ch
== '\\' && QuoteStack
.Up
!= '\\') {
532 if (QuoteStack
.Style
!= BASH_DELIM_LITERAL
)
534 } else if (sc
.ch
== QuoteStack
.Down
) {
536 if (QuoteStack
.Count
== 0) {
537 if (QuoteStack
.Depth
> 0) {
540 sc
.ForwardSetState(SCE_SH_DEFAULT
);
542 } else if (sc
.ch
== QuoteStack
.Up
) {
545 if (QuoteStack
.Style
== BASH_DELIM_STRING
||
546 QuoteStack
.Style
== BASH_DELIM_LSTRING
547 ) { // do nesting for "string", $"locale-string"
549 QuoteStack
.Push(sc
.ch
, BASH_DELIM_BACKTICK
);
550 } else if (sc
.ch
== '$' && sc
.chNext
== '(') {
552 QuoteStack
.Push(sc
.ch
, BASH_DELIM_COMMAND
);
554 } else if (QuoteStack
.Style
== BASH_DELIM_COMMAND
||
555 QuoteStack
.Style
== BASH_DELIM_BACKTICK
556 ) { // do nesting for $(command), `command`
558 QuoteStack
.Push(sc
.ch
, BASH_DELIM_LITERAL
);
559 } else if (sc
.ch
== '\"') {
560 QuoteStack
.Push(sc
.ch
, BASH_DELIM_STRING
);
561 } else if (sc
.ch
== '`') {
562 QuoteStack
.Push(sc
.ch
, BASH_DELIM_BACKTICK
);
563 } else if (sc
.ch
== '$') {
564 if (sc
.chNext
== '\'') {
566 QuoteStack
.Push(sc
.ch
, BASH_DELIM_CSTRING
);
567 } else if (sc
.chNext
== '\"') {
569 QuoteStack
.Push(sc
.ch
, BASH_DELIM_LSTRING
);
570 } else if (sc
.chNext
== '(') {
572 QuoteStack
.Push(sc
.ch
, BASH_DELIM_COMMAND
);
578 case SCE_SH_PARAM
: // ${parameter}
579 if (sc
.ch
== '\\' && Quote
.Up
!= '\\') {
581 } else if (sc
.ch
== Quote
.Down
) {
583 if (Quote
.Count
== 0) {
584 sc
.ForwardSetState(SCE_SH_DEFAULT
);
586 } else if (sc
.ch
== Quote
.Up
) {
590 case SCE_SH_CHARACTER
: // singly-quoted strings
591 if (sc
.ch
== Quote
.Down
) {
593 if (Quote
.Count
== 0) {
594 sc
.ForwardSetState(SCE_SH_DEFAULT
);
600 // Must check end of HereDoc state 1 before default state is handled
601 if (HereDoc
.State
== 1 && sc
.atLineEnd
) {
602 // Begin of here-doc (the line after the here-doc delimiter):
603 // Lexically, the here-doc starts from the next line after the >>, but the
604 // first line of here-doc seem to follow the style of the last EOL sequence
606 if (HereDoc
.Quoted
) {
607 if (sc
.state
== SCE_SH_HERE_DELIM
) {
608 // Missing quote at end of string! Syntax error in bash 4.3
609 // Mark this bit as an error, do not colour any here-doc
610 sc
.ChangeState(SCE_SH_ERROR
);
611 sc
.SetState(SCE_SH_DEFAULT
);
613 // HereDoc.Quote always == '\''
614 sc
.SetState(SCE_SH_HERE_Q
);
616 } else if (HereDoc
.DelimiterLength
== 0) {
617 // no delimiter, illegal (but '' and "" are legal)
618 sc
.ChangeState(SCE_SH_ERROR
);
619 sc
.SetState(SCE_SH_DEFAULT
);
621 sc
.SetState(SCE_SH_HERE_Q
);
625 // update cmdState about the current command segment
626 if (stylePrev
!= SCE_SH_DEFAULT
&& sc
.state
== SCE_SH_DEFAULT
) {
627 cmdState
= cmdStateNew
;
629 // Determine if a new state should be entered.
630 if (sc
.state
== SCE_SH_DEFAULT
) {
632 // Bash can escape any non-newline as a literal
633 sc
.SetState(SCE_SH_IDENTIFIER
);
634 if (sc
.chNext
== '\r' || sc
.chNext
== '\n')
635 sc
.SetState(SCE_SH_OPERATOR
);
636 } else if (IsADigit(sc
.ch
)) {
637 sc
.SetState(SCE_SH_NUMBER
);
638 numBase
= BASH_BASE_DECIMAL
;
639 if (sc
.ch
== '0') { // hex,octal
640 if (sc
.chNext
== 'x' || sc
.chNext
== 'X') {
641 numBase
= BASH_BASE_HEX
;
643 } else if (IsADigit(sc
.chNext
)) {
644 #ifdef PEDANTIC_OCTAL
645 numBase
= BASH_BASE_OCTAL
;
647 numBase
= BASH_BASE_HEX
;
651 } else if (setWordStart
.Contains(sc
.ch
)) {
652 sc
.SetState(SCE_SH_WORD
);
653 } else if (sc
.ch
== '#') {
654 if (stylePrev
!= SCE_SH_WORD
&& stylePrev
!= SCE_SH_IDENTIFIER
&&
655 (sc
.currentPos
== 0 || setMetaCharacter
.Contains(sc
.chPrev
))) {
656 sc
.SetState(SCE_SH_COMMENTLINE
);
658 sc
.SetState(SCE_SH_WORD
);
660 // handle some zsh features within arithmetic expressions only
661 if (cmdState
== BASH_CMD_ARITH
) {
662 if (sc
.chPrev
== '[') { // [#8] [##8] output digit setting
663 sc
.SetState(SCE_SH_WORD
);
664 if (sc
.chNext
== '#') {
667 } else if (sc
.Match("##^") && IsUpperCase(sc
.GetRelative(3))) { // ##^A
668 sc
.SetState(SCE_SH_IDENTIFIER
);
670 } else if (sc
.chNext
== '#' && !IsASpace(sc
.GetRelative(2))) { // ##a
671 sc
.SetState(SCE_SH_IDENTIFIER
);
673 } else if (setWordStart
.Contains(sc
.chNext
)) { // #name
674 sc
.SetState(SCE_SH_IDENTIFIER
);
677 } else if (sc
.ch
== '\"') {
678 sc
.SetState(SCE_SH_STRING
);
679 QuoteStack
.Start(sc
.ch
, BASH_DELIM_STRING
);
680 } else if (sc
.ch
== '\'') {
681 sc
.SetState(SCE_SH_CHARACTER
);
683 } else if (sc
.ch
== '`') {
684 sc
.SetState(SCE_SH_BACKTICKS
);
685 QuoteStack
.Start(sc
.ch
, BASH_DELIM_BACKTICK
);
686 } else if (sc
.ch
== '$') {
687 if (sc
.Match("$((")) {
688 sc
.SetState(SCE_SH_OPERATOR
); // handle '((' later
691 sc
.SetState(SCE_SH_SCALAR
);
694 sc
.ChangeState(SCE_SH_PARAM
);
696 } else if (sc
.ch
== '\'') {
697 sc
.ChangeState(SCE_SH_STRING
);
698 QuoteStack
.Start(sc
.ch
, BASH_DELIM_CSTRING
);
699 } else if (sc
.ch
== '"') {
700 sc
.ChangeState(SCE_SH_STRING
);
701 QuoteStack
.Start(sc
.ch
, BASH_DELIM_LSTRING
);
702 } else if (sc
.ch
== '(') {
703 sc
.ChangeState(SCE_SH_BACKTICKS
);
704 QuoteStack
.Start(sc
.ch
, BASH_DELIM_COMMAND
);
705 } else if (sc
.ch
== '`') { // $` seen in a configure script, valid?
706 sc
.ChangeState(SCE_SH_BACKTICKS
);
707 QuoteStack
.Start(sc
.ch
, BASH_DELIM_BACKTICK
);
709 continue; // scalar has no delimiter pair
711 } else if (sc
.Match('<', '<')) {
712 sc
.SetState(SCE_SH_HERE_DELIM
);
714 if (sc
.GetRelative(2) == '-') { // <<- indent case
715 HereDoc
.Indent
= true;
718 HereDoc
.Indent
= false;
720 } else if (sc
.ch
== '-' && // one-char file test operators
721 setSingleCharOp
.Contains(sc
.chNext
) &&
722 !setWord
.Contains(sc
.GetRelative(2)) &&
723 IsASpace(sc
.chPrev
)) {
724 sc
.SetState(SCE_SH_WORD
);
726 } else if (setBashOperator
.Contains(sc
.ch
)) {
728 bool isCmdDelim
= false;
729 sc
.SetState(SCE_SH_OPERATOR
);
730 // globs have no whitespace, do not appear in arithmetic expressions
731 if (cmdState
!= BASH_CMD_ARITH
&& sc
.ch
== '(' && sc
.chNext
!= '(') {
732 int i
= GlobScan(sc
);
734 sc
.SetState(SCE_SH_IDENTIFIER
);
739 // handle opening delimiters for test/arithmetic expressions - ((,[[,[
740 if (cmdState
== BASH_CMD_START
741 || cmdState
== BASH_CMD_BODY
) {
742 if (sc
.Match('(', '(')) {
743 cmdState
= BASH_CMD_ARITH
;
745 } else if (sc
.Match('[', '[') && IsASpace(sc
.GetRelative(2))) {
746 cmdState
= BASH_CMD_TEST
;
749 } else if (sc
.ch
== '[' && IsASpace(sc
.chNext
)) {
750 cmdState
= BASH_CMD_TEST
;
754 // special state -- for ((x;y;z)) in ... looping
755 if (cmdState
== BASH_CMD_WORD
&& sc
.Match('(', '(')) {
756 cmdState
= BASH_CMD_ARITH
;
760 // handle command delimiters in command START|BODY|WORD state, also TEST if 'test'
761 if (cmdState
== BASH_CMD_START
762 || cmdState
== BASH_CMD_BODY
763 || cmdState
== BASH_CMD_WORD
764 || (cmdState
== BASH_CMD_TEST
&& testExprType
== 0)) {
765 s
[0] = static_cast<char>(sc
.ch
);
766 if (setBashOperator
.Contains(sc
.chNext
)) {
767 s
[1] = static_cast<char>(sc
.chNext
);
769 isCmdDelim
= cmdDelimiter
.InList(s
);
775 isCmdDelim
= cmdDelimiter
.InList(s
);
778 cmdState
= BASH_CMD_DELIM
;
782 // handle closing delimiters for test/arithmetic expressions - )),]],]
783 if (cmdState
== BASH_CMD_ARITH
&& sc
.Match(')', ')')) {
784 cmdState
= BASH_CMD_BODY
;
786 } else if (cmdState
== BASH_CMD_TEST
&& IsASpace(sc
.chPrev
)) {
787 if (sc
.Match(']', ']') && testExprType
== 1) {
789 cmdState
= BASH_CMD_BODY
;
790 } else if (sc
.ch
== ']' && testExprType
== 2) {
791 cmdState
= BASH_CMD_BODY
;
798 if (sc
.state
== SCE_SH_HERE_Q
) {
799 styler
.ChangeLexerState(sc
.currentPos
, styler
.Length());
804 static bool IsCommentLine(Sci_Position line
, Accessor
&styler
) {
805 Sci_Position pos
= styler
.LineStart(line
);
806 Sci_Position eol_pos
= styler
.LineStart(line
+ 1) - 1;
807 for (Sci_Position i
= pos
; i
< eol_pos
; i
++) {
811 else if (ch
!= ' ' && ch
!= '\t')
817 static void FoldBashDoc(Sci_PositionU startPos
, Sci_Position length
, int, WordList
*[],
819 bool foldComment
= styler
.GetPropertyInt("fold.comment") != 0;
820 bool foldCompact
= styler
.GetPropertyInt("fold.compact", 1) != 0;
821 Sci_PositionU endPos
= startPos
+ length
;
822 int visibleChars
= 0;
824 Sci_Position lineCurrent
= styler
.GetLine(startPos
);
825 int levelPrev
= styler
.LevelAt(lineCurrent
) & SC_FOLDLEVELNUMBERMASK
;
826 int levelCurrent
= levelPrev
;
827 char chNext
= styler
[startPos
];
828 int styleNext
= styler
.StyleAt(startPos
);
829 for (Sci_PositionU i
= startPos
; i
< endPos
; i
++) {
831 chNext
= styler
.SafeGetCharAt(i
+ 1);
832 int style
= styleNext
;
833 styleNext
= styler
.StyleAt(i
+ 1);
834 bool atEOL
= (ch
== '\r' && chNext
!= '\n') || (ch
== '\n');
836 if (foldComment
&& atEOL
&& IsCommentLine(lineCurrent
, styler
))
838 if (!IsCommentLine(lineCurrent
- 1, styler
)
839 && IsCommentLine(lineCurrent
+ 1, styler
))
841 else if (IsCommentLine(lineCurrent
- 1, styler
)
842 && !IsCommentLine(lineCurrent
+ 1, styler
))
845 if (style
== SCE_SH_OPERATOR
) {
848 } else if (ch
== '}') {
852 // Here Document folding
853 if (style
== SCE_SH_HERE_DELIM
) {
854 if (ch
== '<' && chNext
== '<') {
855 if (styler
.SafeGetCharAt(i
+ 2) == '<') {
858 if (skipHereCh
== 0) {
865 } else if (style
== SCE_SH_HERE_Q
&& styler
.StyleAt(i
+1) == SCE_SH_DEFAULT
) {
870 if (visibleChars
== 0 && foldCompact
)
871 lev
|= SC_FOLDLEVELWHITEFLAG
;
872 if ((levelCurrent
> levelPrev
) && (visibleChars
> 0))
873 lev
|= SC_FOLDLEVELHEADERFLAG
;
874 if (lev
!= styler
.LevelAt(lineCurrent
)) {
875 styler
.SetLevel(lineCurrent
, lev
);
878 levelPrev
= levelCurrent
;
881 if (!isspacechar(ch
))
884 // Fill in the real level of the next line, keeping the current flags as they will be filled in later
885 int flagsNext
= styler
.LevelAt(lineCurrent
) & ~SC_FOLDLEVELNUMBERMASK
;
886 styler
.SetLevel(lineCurrent
, levelPrev
| flagsNext
);
889 static const char * const bashWordListDesc
[] = {
894 LexerModule
lmBash(SCLEX_BASH
, ColouriseBashDoc
, "bash", FoldBashDoc
, bashWordListDesc
);