Update of Swedish translation
[geany-mirror.git] / scintilla / lexers / LexBash.cxx
blob9c02c2897ee0572f0b8849af7558de5376875181
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 int GlobScan(StyleContext &sc) {
100 // forward scan for zsh globs, disambiguate versus bash arrays
101 // complex expressions may still fail, e.g. unbalanced () '' "" etc
102 int c, sLen = 0;
103 int pCount = 0;
104 int hash = 0;
105 while ((c = sc.GetRelativeCharacter(++sLen)) != 0) {
106 if (IsASpace(c)) {
107 return 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 == '(') {
113 pCount++;
114 } else if (c == ')') {
115 if (pCount == 0) {
116 if (hash) return sLen;
117 return 0;
119 pCount--;
122 return 0;
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
147 public:
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
156 HereDocCls() {
157 State = 0;
158 Quote = 0;
159 Quoted = false;
160 Indent = 0;
161 DelimiterLength = 0;
162 Delimiter[0] = '\0';
164 void Append(int ch) {
165 Delimiter[DelimiterLength++] = static_cast<char>(ch);
166 Delimiter[DelimiterLength] = '\0';
168 ~HereDocCls() {
171 HereDocCls HereDoc;
173 class QuoteCls { // Class to manage quote pairs (simplified vs LexPerl)
174 public:
175 int Count;
176 int Up, Down;
177 QuoteCls() {
178 Count = 0;
179 Up = '\0';
180 Down = '\0';
182 void Open(int u) {
183 Count++;
184 Up = u;
185 Down = opposite(Up);
187 void Start(int u) {
188 Count = 0;
189 Open(u);
192 QuoteCls Quote;
194 class QuoteStackCls { // Class to manage quote pairs that nest
195 public:
196 int Count;
197 int Up, Down;
198 int Style;
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];
203 QuoteStackCls() {
204 Count = 0;
205 Up = '\0';
206 Down = '\0';
207 Style = 0;
208 Depth = 0;
210 void Start(int u, int s) {
211 Count = 1;
212 Up = u;
213 Down = opposite(Up);
214 Style = s;
216 void Push(int u, int s) {
217 if (Depth >= BASH_DELIM_STACK_MAX)
218 return;
219 CountStack[Depth] = Count;
220 UpStack [Depth] = Up;
221 StyleStack[Depth] = Style;
222 Depth++;
223 Count = 1;
224 Up = u;
225 Down = opposite(Up);
226 Style = s;
228 void Pop(void) {
229 if (Depth <= 0)
230 return;
231 Depth--;
232 Count = CountStack[Depth];
233 Up = UpStack [Depth];
234 Style = StyleStack[Depth];
235 Down = opposite(Up);
237 ~QuoteStackCls() {
240 QuoteStackCls QuoteStack;
242 int numBase = 0;
243 int digit;
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)))
252 ln--;
253 for (;;) {
254 startPos = styler.LineStart(ln);
255 if (ln == 0 || styler.GetLineState(ln) == BASH_CMD_START)
256 break;
257 ln--;
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);
276 } else {
277 if (ln > 0) {
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
281 } else
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.
297 switch (sc.state) {
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;
304 break;
305 case SCE_SH_WORD:
306 // "." never used in Bash variable names but used in file names
307 if (!setWord.Contains(sc.ch)) {
308 char s[500];
309 char s2[10];
310 sc.GetCurrent(s, sizeof(s));
311 // allow keywords ending in a whitespace or command delimiter
312 s2[0] = static_cast<char>(sc.ch);
313 s2[1] = '\0';
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;
321 else
322 sc.ChangeState(SCE_SH_IDENTIFIER);
323 sc.SetState(SCE_SH_DEFAULT);
324 break;
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;
330 testExprType = 0;
331 } else
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;
338 else
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;
345 else
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);
360 break;
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);
369 break;
370 case SCE_SH_NUMBER:
371 digit = translateBashDigit(sc.ch);
372 if (numBase == BASH_BASE_DECIMAL) {
373 if (sc.ch == '#') {
374 char s[10];
375 sc.GetCurrent(s, sizeof(s));
376 numBase = getBashNumberBase(s);
377 if (numBase != BASH_BASE_ERROR)
378 break;
379 } else if (IsADigit(sc.ch))
380 break;
381 } else if (numBase == BASH_BASE_HEX) {
382 if (IsADigit(sc.ch, 16))
383 break;
384 #ifdef PEDANTIC_OCTAL
385 } else if (numBase == BASH_BASE_OCTAL ||
386 numBase == BASH_BASE_OCTAL_ERROR) {
387 if (digit <= 7)
388 break;
389 if (digit <= 9) {
390 numBase = BASH_BASE_OCTAL_ERROR;
391 break;
393 #endif
394 } else if (numBase == BASH_BASE_ERROR) {
395 if (digit <= 9)
396 break;
397 } else { // DD#DDDD number style handling
398 if (digit != BASH_BASE_ERROR) {
399 if (numBase <= 36) {
400 // case-insensitive if base<=36
401 if (digit >= 36) digit -= 26;
403 if (digit < numBase)
404 break;
405 if (digit <= 9) {
406 numBase = BASH_BASE_ERROR;
407 break;
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
415 #endif
417 sc.ChangeState(SCE_SH_ERROR);
419 sc.SetState(SCE_SH_DEFAULT);
420 break;
421 case SCE_SH_COMMENTLINE:
422 if (sc.atLineEnd && sc.chPrev != '\\') {
423 sc.SetState(SCE_SH_DEFAULT);
425 break;
426 case SCE_SH_HERE_DELIM:
427 // From Bash info:
428 // ---------------
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 ")
439 sc.Forward();
440 HereDoc.Quoted = true;
441 HereDoc.State = 1;
442 } else if (setHereDoc.Contains(sc.chNext) ||
443 (sc.chNext == '=' && cmdState != BASH_CMD_ARITH)) {
444 // an unquoted here-doc delimiter, no special handling
445 HereDoc.State = 1;
446 } else if (sc.chNext == '<') { // HERE string <<<
447 sc.Forward();
448 sc.ForwardSetState(SCE_SH_DEFAULT);
449 } else if (IsASpace(sc.chNext)) {
450 // eat whitespace
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);
456 } else {
457 // symbols terminates; deprecated zero-length delimiter
458 HereDoc.State = 1;
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);
474 } else {
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);
482 HereDoc.State = 0;
485 break;
486 case SCE_SH_HERE_Q:
487 // HereDoc.State == 2
488 if (sc.atLineStart) {
489 sc.SetState(SCE_SH_HERE_Q);
490 int prefixws = 0;
491 while (sc.ch == '\t' && !sc.atLineEnd) { // tabulation prefix
492 sc.Forward();
493 prefixws++;
495 if (prefixws > 0)
496 sc.SetState(SCE_SH_HERE_Q);
497 while (!sc.atLineEnd) {
498 sc.Forward();
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);
506 break;
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);
514 break;
518 break;
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);
524 } else {
525 sc.SetState(SCE_SH_DEFAULT);
528 break;
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)
533 sc.Forward();
534 } else if (sc.ch == QuoteStack.Down) {
535 QuoteStack.Count--;
536 if (QuoteStack.Count == 0) {
537 if (QuoteStack.Depth > 0) {
538 QuoteStack.Pop();
539 } else
540 sc.ForwardSetState(SCE_SH_DEFAULT);
542 } else if (sc.ch == QuoteStack.Up) {
543 QuoteStack.Count++;
544 } else {
545 if (QuoteStack.Style == BASH_DELIM_STRING ||
546 QuoteStack.Style == BASH_DELIM_LSTRING
547 ) { // do nesting for "string", $"locale-string"
548 if (sc.ch == '`') {
549 QuoteStack.Push(sc.ch, BASH_DELIM_BACKTICK);
550 } else if (sc.ch == '$' && sc.chNext == '(') {
551 sc.Forward();
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`
557 if (sc.ch == '\'') {
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 == '\'') {
565 sc.Forward();
566 QuoteStack.Push(sc.ch, BASH_DELIM_CSTRING);
567 } else if (sc.chNext == '\"') {
568 sc.Forward();
569 QuoteStack.Push(sc.ch, BASH_DELIM_LSTRING);
570 } else if (sc.chNext == '(') {
571 sc.Forward();
572 QuoteStack.Push(sc.ch, BASH_DELIM_COMMAND);
577 break;
578 case SCE_SH_PARAM: // ${parameter}
579 if (sc.ch == '\\' && Quote.Up != '\\') {
580 sc.Forward();
581 } else if (sc.ch == Quote.Down) {
582 Quote.Count--;
583 if (Quote.Count == 0) {
584 sc.ForwardSetState(SCE_SH_DEFAULT);
586 } else if (sc.ch == Quote.Up) {
587 Quote.Count++;
589 break;
590 case SCE_SH_CHARACTER: // singly-quoted strings
591 if (sc.ch == Quote.Down) {
592 Quote.Count--;
593 if (Quote.Count == 0) {
594 sc.ForwardSetState(SCE_SH_DEFAULT);
597 break;
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
605 HereDoc.State = 2;
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);
612 } else {
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);
620 } else {
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) {
631 if (sc.ch == '\\') {
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;
642 sc.Forward();
643 } else if (IsADigit(sc.chNext)) {
644 #ifdef PEDANTIC_OCTAL
645 numBase = BASH_BASE_OCTAL;
646 #else
647 numBase = BASH_BASE_HEX;
648 #endif
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);
657 } else {
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 == '#') {
665 sc.Forward();
667 } else if (sc.Match("##^") && IsUpperCase(sc.GetRelative(3))) { // ##^A
668 sc.SetState(SCE_SH_IDENTIFIER);
669 sc.Forward(3);
670 } else if (sc.chNext == '#' && !IsASpace(sc.GetRelative(2))) { // ##a
671 sc.SetState(SCE_SH_IDENTIFIER);
672 sc.Forward(2);
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);
682 Quote.Start(sc.ch);
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
689 continue;
691 sc.SetState(SCE_SH_SCALAR);
692 sc.Forward();
693 if (sc.ch == '{') {
694 sc.ChangeState(SCE_SH_PARAM);
695 Quote.Start(sc.ch);
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);
708 } else {
709 continue; // scalar has no delimiter pair
711 } else if (sc.Match('<', '<')) {
712 sc.SetState(SCE_SH_HERE_DELIM);
713 HereDoc.State = 0;
714 if (sc.GetRelative(2) == '-') { // <<- indent case
715 HereDoc.Indent = true;
716 sc.Forward();
717 } else {
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);
725 sc.Forward();
726 } else if (setBashOperator.Contains(sc.ch)) {
727 char s[10];
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);
733 if (i > 1) {
734 sc.SetState(SCE_SH_IDENTIFIER);
735 sc.Forward(i);
736 continue;
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;
744 sc.Forward();
745 } else if (sc.Match('[', '[') && IsASpace(sc.GetRelative(2))) {
746 cmdState = BASH_CMD_TEST;
747 testExprType = 1;
748 sc.Forward();
749 } else if (sc.ch == '[' && IsASpace(sc.chNext)) {
750 cmdState = BASH_CMD_TEST;
751 testExprType = 2;
754 // special state -- for ((x;y;z)) in ... looping
755 if (cmdState == BASH_CMD_WORD && sc.Match('(', '(')) {
756 cmdState = BASH_CMD_ARITH;
757 sc.Forward();
758 continue;
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);
768 s[2] = '\0';
769 isCmdDelim = cmdDelimiter.InList(s);
770 if (isCmdDelim)
771 sc.Forward();
773 if (!isCmdDelim) {
774 s[1] = '\0';
775 isCmdDelim = cmdDelimiter.InList(s);
777 if (isCmdDelim) {
778 cmdState = BASH_CMD_DELIM;
779 continue;
782 // handle closing delimiters for test/arithmetic expressions - )),]],]
783 if (cmdState == BASH_CMD_ARITH && sc.Match(')', ')')) {
784 cmdState = BASH_CMD_BODY;
785 sc.Forward();
786 } else if (cmdState == BASH_CMD_TEST && IsASpace(sc.chPrev)) {
787 if (sc.Match(']', ']') && testExprType == 1) {
788 sc.Forward();
789 cmdState = BASH_CMD_BODY;
790 } else if (sc.ch == ']' && testExprType == 2) {
791 cmdState = BASH_CMD_BODY;
795 }// sc.state
797 sc.Complete();
798 if (sc.state == SCE_SH_HERE_Q) {
799 styler.ChangeLexerState(sc.currentPos, styler.Length());
801 sc.Complete();
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++) {
808 char ch = styler[i];
809 if (ch == '#')
810 return true;
811 else if (ch != ' ' && ch != '\t')
812 return false;
814 return false;
817 static void FoldBashDoc(Sci_PositionU startPos, Sci_Position length, int, WordList *[],
818 Accessor &styler) {
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;
823 int skipHereCh = 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++) {
830 char ch = chNext;
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');
835 // Comment folding
836 if (foldComment && atEOL && IsCommentLine(lineCurrent, styler))
838 if (!IsCommentLine(lineCurrent - 1, styler)
839 && IsCommentLine(lineCurrent + 1, styler))
840 levelCurrent++;
841 else if (IsCommentLine(lineCurrent - 1, styler)
842 && !IsCommentLine(lineCurrent + 1, styler))
843 levelCurrent--;
845 if (style == SCE_SH_OPERATOR) {
846 if (ch == '{') {
847 levelCurrent++;
848 } else if (ch == '}') {
849 levelCurrent--;
852 // Here Document folding
853 if (style == SCE_SH_HERE_DELIM) {
854 if (ch == '<' && chNext == '<') {
855 if (styler.SafeGetCharAt(i + 2) == '<') {
856 skipHereCh = 1;
857 } else {
858 if (skipHereCh == 0) {
859 levelCurrent++;
860 } else {
861 skipHereCh = 0;
865 } else if (style == SCE_SH_HERE_Q && styler.StyleAt(i+1) == SCE_SH_DEFAULT) {
866 levelCurrent--;
868 if (atEOL) {
869 int lev = levelPrev;
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);
877 lineCurrent++;
878 levelPrev = levelCurrent;
879 visibleChars = 0;
881 if (!isspacechar(ch))
882 visibleChars++;
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[] = {
890 "Keywords",
894 LexerModule lmBash(SCLEX_BASH, ColouriseBashDoc, "bash", FoldBashDoc, bashWordListDesc);