1 // Scintilla\ source code edit control
3 ** Lexer for Take Command / TCC batch scripts (.bat, .btm, .cmd).
5 // Written by Rex Conn (rconn [at] jpsoft [dot] com)
6 // based on the CMD lexer
7 // The License.txt file describes the conditions under which this software may be distributed.
17 #include "Scintilla.h"
21 #include "LexAccessor.h"
23 #include "StyleContext.h"
24 #include "CharacterSet.h"
25 #include "LexerModule.h"
28 using namespace Scintilla
;
32 static bool IsAlphabetic(int ch
) {
33 return IsASCII(ch
) && isalpha(ch
);
36 static inline bool AtEOL(Accessor
&styler
, unsigned int i
) {
37 return (styler
[i
] == '\n') || ((styler
[i
] == '\r') && (styler
.SafeGetCharAt(i
+ 1) != '\n'));
40 // Tests for BATCH Operators
41 static bool IsBOperator(char ch
) {
42 return (ch
== '=') || (ch
== '+') || (ch
== '>') || (ch
== '<') || (ch
== '|') || (ch
== '&') || (ch
== '!') || (ch
== '?') || (ch
== '*') || (ch
== '(') || (ch
== ')');
45 // Tests for BATCH Separators
46 static bool IsBSeparator(char ch
) {
47 return (ch
== '\\') || (ch
== '.') || (ch
== ';') || (ch
== ' ') || (ch
== '\t') || (ch
== '[') || (ch
== ']') || (ch
== '\"') || (ch
== '\'') || (ch
== '/');
50 // Find length of CMD FOR variable with modifier (%~...) or return 0
51 static unsigned int GetBatchVarLen( char *wordBuffer
)
54 if ( wordBuffer
[0] == '%' ) {
56 if ( wordBuffer
[1] == '~' )
58 else if (( wordBuffer
[1] == '%' ) && ( wordBuffer
[2] == '~' ))
63 for ( ; ( wordBuffer
[nLength
] ); nLength
++ ) {
65 switch ( toupper(wordBuffer
[nLength
]) ) {
71 // fully qualified path name
79 // date / time of file
81 // file extension only
95 static void ColouriseTCMDLine( char *lineBuffer
, unsigned int lengthLine
, unsigned int startLine
, unsigned int endPos
, WordList
*keywordlists
[], Accessor
&styler
)
97 unsigned int offset
= 0; // Line Buffer Offset
98 char wordBuffer
[260]; // Word Buffer - large to catch long paths
99 unsigned int wbl
; // Word Buffer Length
100 unsigned int wbo
; // Word Buffer Offset - also Special Keyword Buffer Length
101 WordList
&keywords
= *keywordlists
[0]; // Internal Commands
102 // WordList &keywords2 = *keywordlists[1]; // Aliases (optional)
103 bool isDelayedExpansion
= 1; // !var!
105 bool continueProcessing
= true; // Used to toggle Regular Keyword Checking
106 // Special Keywords are those that allow certain characters without whitespace after the command
107 // Examples are: cd. cd\ echo: echo. path=
108 bool inString
= false; // Used for processing while ""
109 // Special Keyword Buffer used to determine if the first n characters is a Keyword
110 char sKeywordBuffer
[260] = ""; // Special Keyword Buffer
111 bool sKeywordFound
; // Exit Special Keyword for-loop if found
113 // Skip leading whitespace
114 while ((offset
< lengthLine
) && (isspacechar(lineBuffer
[offset
]))) {
117 // Colorize Default Text
118 styler
.ColourTo(startLine
+ offset
- 1, SCE_TCMD_DEFAULT
);
120 if ( offset
>= lengthLine
)
123 // Check for Fake Label (Comment) or Real Label - return if found
124 if (lineBuffer
[offset
] == ':') {
125 if (lineBuffer
[offset
+ 1] == ':') {
126 // Colorize Fake Label (Comment) - :: is the same as REM
127 styler
.ColourTo(endPos
, SCE_TCMD_COMMENT
);
129 // Colorize Real Label
130 styler
.ColourTo(endPos
, SCE_TCMD_LABEL
);
134 // Check for Comment - return if found
135 } else if (( CompareNCaseInsensitive(lineBuffer
+offset
, "rem", 3) == 0 ) && (( lineBuffer
[offset
+3] == 0 ) || ( isspace(lineBuffer
[offset
+3] )))) {
136 styler
.ColourTo(endPos
, SCE_TCMD_COMMENT
);
139 // Check for Drive Change (Drive Change is internal command) - return if found
140 } else if ((IsAlphabetic(lineBuffer
[offset
])) &&
141 (lineBuffer
[offset
+ 1] == ':') &&
142 ((isspacechar(lineBuffer
[offset
+ 2])) ||
143 (((lineBuffer
[offset
+ 2] == '\\')) &&
144 (isspacechar(lineBuffer
[offset
+ 3]))))) {
145 // Colorize Regular Keyword
146 styler
.ColourTo(endPos
, SCE_TCMD_WORD
);
150 // Check for Hide Command (@ECHO OFF/ON)
151 if (lineBuffer
[offset
] == '@') {
152 styler
.ColourTo(startLine
+ offset
, SCE_TCMD_HIDE
);
156 while ((offset
< lengthLine
) && (isspacechar(lineBuffer
[offset
]))) {
160 // Read remainder of line word-at-a-time or remainder-of-word-at-a-time
161 while (offset
< lengthLine
) {
162 if (offset
> startLine
) {
163 // Colorize Default Text
164 styler
.ColourTo(startLine
+ offset
- 1, SCE_TCMD_DEFAULT
);
166 // Copy word from Line Buffer into Word Buffer
168 for (; offset
< lengthLine
&& ( wbl
< 260 ) && !isspacechar(lineBuffer
[offset
]); wbl
++, offset
++) {
169 wordBuffer
[wbl
] = static_cast<char>(tolower(lineBuffer
[offset
]));
171 wordBuffer
[wbl
] = '\0';
174 // Check for Separator
175 if (IsBSeparator(wordBuffer
[0])) {
177 // Reset Offset to re-process remainder of word
179 // Colorize Default Text
180 styler
.ColourTo(startLine
+ offset
- 1, SCE_BAT_DEFAULT
);
182 if (wordBuffer
[0] == '"')
183 inString
= !inString
;
185 // Check for Regular expression
186 } else if (( wordBuffer
[0] == ':' ) && ( wordBuffer
[1] == ':' ) && (continueProcessing
)) {
188 // Colorize Regular exoressuin
189 styler
.ColourTo(startLine
+ offset
- 1, SCE_TCMD_DEFAULT
);
190 // No need to Reset Offset
192 // Check for Labels in text (... :label)
193 } else if (wordBuffer
[0] == ':' && isspacechar(lineBuffer
[offset
- wbl
- 1])) {
194 // Colorize Default Text
195 styler
.ColourTo(startLine
+ offset
- 1 - wbl
, SCE_TCMD_DEFAULT
);
197 styler
.ColourTo(startLine
+ offset
- 1, SCE_TCMD_CLABEL
);
198 // No need to Reset Offset
199 // Check for delayed expansion Variable (!x...!)
200 } else if (isDelayedExpansion
&& wordBuffer
[0] == '!') {
201 // Colorize Default Text
202 styler
.ColourTo(startLine
+ offset
- 1 - wbl
, SCE_TCMD_DEFAULT
);
204 // Search to end of word for second !
205 while ((wbo
< wbl
) && (wordBuffer
[wbo
] != '!') && (!IsBOperator(wordBuffer
[wbo
])) && (!IsBSeparator(wordBuffer
[wbo
]))) {
208 if (wordBuffer
[wbo
] == '!') {
210 // Colorize Environment Variable
211 styler
.ColourTo(startLine
+ offset
- 1 - (wbl
- wbo
), SCE_TCMD_EXPANSION
);
215 styler
.ColourTo(startLine
+ offset
- 1 - (wbl
- 1), SCE_TCMD_DEFAULT
);
218 // Reset Offset to re-process remainder of word
219 offset
-= (wbl
- wbo
);
221 // Check for Regular Keyword in list
222 } else if ((keywords
.InList(wordBuffer
)) && (!inString
) && (continueProcessing
)) {
224 // ECHO, PATH, and PROMPT require no further Regular Keyword Checking
225 if ((CompareCaseInsensitive(wordBuffer
, "echo") == 0) ||
226 (CompareCaseInsensitive(sKeywordBuffer
, "echos") == 0) ||
227 (CompareCaseInsensitive(sKeywordBuffer
, "echoerr") == 0) ||
228 (CompareCaseInsensitive(sKeywordBuffer
, "echoserr") == 0) ||
229 (CompareCaseInsensitive(wordBuffer
, "path") == 0) ||
230 (CompareCaseInsensitive(wordBuffer
, "prompt") == 0)) {
231 continueProcessing
= false;
234 // Colorize Regular keyword
235 styler
.ColourTo(startLine
+ offset
- 1, SCE_TCMD_WORD
);
236 // No need to Reset Offset
238 } else if ((wordBuffer
[0] != '%') && (wordBuffer
[0] != '!') && (!IsBOperator(wordBuffer
[0])) && (!inString
) && (continueProcessing
)) {
240 // a few commands accept "illegal" syntax -- cd\, echo., etc.
241 sscanf( wordBuffer
, "%[^.<>|&=\\/]", sKeywordBuffer
);
242 sKeywordFound
= false;
244 if ((CompareCaseInsensitive(sKeywordBuffer
, "echo") == 0) ||
245 (CompareCaseInsensitive(sKeywordBuffer
, "echos") == 0) ||
246 (CompareCaseInsensitive(sKeywordBuffer
, "echoerr") == 0) ||
247 (CompareCaseInsensitive(sKeywordBuffer
, "echoserr") == 0) ||
248 (CompareCaseInsensitive(sKeywordBuffer
, "cd") == 0) ||
249 (CompareCaseInsensitive(sKeywordBuffer
, "path") == 0) ||
250 (CompareCaseInsensitive(sKeywordBuffer
, "prompt") == 0)) {
252 // no further Regular Keyword Checking
253 continueProcessing
= false;
254 sKeywordFound
= true;
255 wbo
= (unsigned int)strlen( sKeywordBuffer
);
257 // Colorize Special Keyword as Regular Keyword
258 styler
.ColourTo(startLine
+ offset
- 1 - (wbl
- wbo
), SCE_TCMD_WORD
);
259 // Reset Offset to re-process remainder of word
260 offset
-= (wbl
- wbo
);
263 // Check for Default Text
264 if (!sKeywordFound
) {
266 // Read up to %, Operator or Separator
267 while ((wbo
< wbl
) && (wordBuffer
[wbo
] != '%') && (!isDelayedExpansion
|| wordBuffer
[wbo
] != '!') && (!IsBOperator(wordBuffer
[wbo
])) && (!IsBSeparator(wordBuffer
[wbo
]))) {
270 // Colorize Default Text
271 styler
.ColourTo(startLine
+ offset
- 1 - (wbl
- wbo
), SCE_TCMD_DEFAULT
);
272 // Reset Offset to re-process remainder of word
273 offset
-= (wbl
- wbo
);
276 // Check for Argument (%n), Environment Variable (%x...%) or Local Variable (%%a)
277 } else if (wordBuffer
[0] == '%') {
280 // Colorize Default Text
281 styler
.ColourTo(startLine
+ offset
- 1 - wbl
, SCE_TCMD_DEFAULT
);
284 // check for %[nn] syntax
285 if ( wordBuffer
[1] == '[' ) {
287 while ((n
< wbl
) && (wordBuffer
[n
] != ']')) {
290 if ( wordBuffer
[n
] == ']' )
295 // Search to end of word for second % or to the first terminator (can be a long path)
296 while ((wbo
< wbl
) && (wordBuffer
[wbo
] != '%') && (!IsBOperator(wordBuffer
[wbo
])) && (!IsBSeparator(wordBuffer
[wbo
]))) {
300 // Check for Argument (%n) or (%*)
301 if (((isdigit(wordBuffer
[1])) || (wordBuffer
[1] == '*')) && (wordBuffer
[wbo
] != '%')) {
302 while (( wordBuffer
[n
] ) && ( strchr( "%0123456789*#$", wordBuffer
[n
] ) != NULL
))
306 styler
.ColourTo(startLine
+ offset
- 1 - (wbl
- n
), SCE_TCMD_IDENTIFIER
);
307 // Reset Offset to re-process remainder of word
310 // Check for Variable with modifiers (%~...)
311 } else if ((varlen
= GetBatchVarLen(wordBuffer
)) != 0) {
314 styler
.ColourTo(startLine
+ offset
- 1 - (wbl
- varlen
), SCE_TCMD_IDENTIFIER
);
315 // Reset Offset to re-process remainder of word
316 offset
-= (wbl
- varlen
);
318 // Check for Environment Variable (%x...%)
319 } else if (( wordBuffer
[1] ) && ( wordBuffer
[1] != '%')) {
320 if ( wordBuffer
[wbo
] == '%' )
323 // Colorize Environment Variable
324 styler
.ColourTo(startLine
+ offset
- 1 - (wbl
- wbo
), SCE_TCMD_ENVIRONMENT
);
325 // Reset Offset to re-process remainder of word
326 offset
-= (wbl
- wbo
);
328 // Check for Local Variable (%%a)
329 } else if ( (wbl
> 2) && (wordBuffer
[1] == '%') && (wordBuffer
[2] != '%') && (!IsBOperator(wordBuffer
[2])) && (!IsBSeparator(wordBuffer
[2]))) {
332 while (( wordBuffer
[n
] ) && (!IsBOperator(wordBuffer
[n
])) && (!IsBSeparator(wordBuffer
[n
])))
335 // Colorize Local Variable
336 styler
.ColourTo(startLine
+ offset
- 1 - (wbl
- n
), SCE_TCMD_IDENTIFIER
);
337 // Reset Offset to re-process remainder of word
341 } else if ((wbl
> 1) && (wordBuffer
[1] == '%')) {
344 styler
.ColourTo(startLine
+ offset
- 1 - (wbl
- 2), SCE_TCMD_DEFAULT
);
345 // Reset Offset to re-process remainder of word
350 styler
.ColourTo(startLine
+ offset
- 1 - (wbl
- 1), SCE_TCMD_DEFAULT
);
351 // Reset Offset to re-process remainder of word
355 // Check for Operator
356 } else if (IsBOperator(wordBuffer
[0])) {
357 // Colorize Default Text
358 styler
.ColourTo(startLine
+ offset
- 1 - wbl
, SCE_TCMD_DEFAULT
);
360 // Check for Pipe, compound, or conditional Operator
361 if ((wordBuffer
[0] == '|') || (wordBuffer
[0] == '&')) {
363 // Colorize Pipe Operator
364 styler
.ColourTo(startLine
+ offset
- 1 - (wbl
- 1), SCE_TCMD_OPERATOR
);
365 // Reset Offset to re-process remainder of word
367 continueProcessing
= true;
369 // Check for Other Operator
371 // Check for > Operator
372 if ((wordBuffer
[0] == '>') || (wordBuffer
[0] == '<')) {
373 // Turn Keyword and External Command / Program checking back on
374 continueProcessing
= true;
376 // Colorize Other Operator
377 if (!inString
|| !(wordBuffer
[0] == '(' || wordBuffer
[0] == ')'))
378 styler
.ColourTo(startLine
+ offset
- 1 - (wbl
- 1), SCE_TCMD_OPERATOR
);
379 // Reset Offset to re-process remainder of word
383 // Check for Default Text
385 // Read up to %, Operator or Separator
386 while ((wbo
< wbl
) && (wordBuffer
[wbo
] != '%') && (!isDelayedExpansion
|| wordBuffer
[wbo
] != '!') && (!IsBOperator(wordBuffer
[wbo
])) && (!IsBSeparator(wordBuffer
[wbo
]))) {
389 // Colorize Default Text
390 styler
.ColourTo(startLine
+ offset
- 1 - (wbl
- wbo
), SCE_TCMD_DEFAULT
);
391 // Reset Offset to re-process remainder of word
392 offset
-= (wbl
- wbo
);
395 // Skip whitespace - nothing happens if Offset was Reset
396 while ((offset
< lengthLine
) && (isspacechar(lineBuffer
[offset
]))) {
400 // Colorize Default Text for remainder of line - currently not lexed
401 styler
.ColourTo(endPos
, SCE_TCMD_DEFAULT
);
404 static void ColouriseTCMDDoc( unsigned int startPos
, int length
, int /*initStyle*/, WordList
*keywordlists
[], Accessor
&styler
)
406 char lineBuffer
[16384];
408 styler
.StartAt(startPos
);
409 styler
.StartSegment(startPos
);
410 unsigned int linePos
= 0;
411 unsigned int startLine
= startPos
;
412 for (unsigned int i
= startPos
; i
< startPos
+ length
; i
++) {
413 lineBuffer
[linePos
++] = styler
[i
];
414 if (AtEOL(styler
, i
) || (linePos
>= sizeof(lineBuffer
) - 1)) {
415 // End of line (or of line buffer) met, colourise it
416 lineBuffer
[linePos
] = '\0';
417 ColouriseTCMDLine(lineBuffer
, linePos
, startLine
, i
, keywordlists
, styler
);
422 if (linePos
> 0) { // Last line does not have ending characters
423 lineBuffer
[linePos
] = '\0';
424 ColouriseTCMDLine(lineBuffer
, linePos
, startLine
, startPos
+ length
- 1, keywordlists
, styler
);
428 // Convert string to upper case
429 static void StrUpr(char *s
) {
431 *s
= MakeUpperCase(*s
);
436 // Folding support (for DO, IFF, SWITCH, TEXT, and command groups)
437 static void FoldTCMDDoc(unsigned int startPos
, int length
, int, WordList
*[], Accessor
&styler
)
439 int line
= styler
.GetLine(startPos
);
440 int level
= styler
.LevelAt(line
);
442 unsigned int endPos
= startPos
+ length
;
445 char chPrev
= styler
.SafeGetCharAt(startPos
- 1);
448 for (unsigned int i
= startPos
; i
< endPos
; i
++) {
450 int c
= styler
.SafeGetCharAt(i
, '\n');
451 int style
= styler
.StyleAt(i
);
452 bool bLineStart
= ((chPrev
== '\r') || (chPrev
== '\n')) || i
== 0;
454 if (style
== SCE_TCMD_OPERATOR
) {
458 } else if (c
== ')') {
463 if (( bLineStart
) && ( style
== SCE_TCMD_WORD
)) {
464 for (unsigned int j
= 0; j
< 10; j
++) {
465 if (!iswordchar(styler
[i
+ j
])) {
468 s
[j
] = styler
[i
+ j
];
473 if ((strcmp(s
, "DO") == 0) || (strcmp(s
, "IFF") == 0) || (strcmp(s
, "SWITCH") == 0) || (strcmp(s
, "TEXT") == 0)) {
475 } else if ((strcmp(s
, "ENDDO") == 0) || (strcmp(s
, "ENDIFF") == 0) || (strcmp(s
, "ENDSWITCH") == 0) || (strcmp(s
, "ENDTEXT") == 0)) {
480 if (c
== '\n') { // line end
481 if (levelIndent
> 0) {
482 level
|= SC_FOLDLEVELHEADERFLAG
;
484 if (level
!= styler
.LevelAt(line
))
485 styler
.SetLevel(line
, level
);
486 level
+= levelIndent
;
487 if ((level
& SC_FOLDLEVELNUMBERMASK
) < SC_FOLDLEVELBASE
)
488 level
= SC_FOLDLEVELBASE
;
492 level
&= ~SC_FOLDLEVELHEADERFLAG
;
493 level
&= ~SC_FOLDLEVELWHITEFLAG
;
500 static const char *const tcmdWordListDesc
[] = {
506 LexerModule
lmTCMD(SCLEX_TCMD
, ColouriseTCMDDoc
, "tcmd", FoldTCMDDoc
, tcmdWordListDesc
);