on 8d0aa39 help sign in
bat/assets/syntaxes/fish.sublime-syntax
1009 lines 46 kB history raw
1%YAML 1.2
2---
3# http://www.sublimetext.com/docs/3/syntax.html
4name: friendly interactive shell (fish)
5file_extensions:
6 - fish
7first_line_match: '^#!.*\b(fish)|^#\s*-\*-[^*]*mode:\s*shell-script[^*]*-\*-'
8scope: source.shell.fish
9contexts:
10 main:
11 - include: comment-external
12 - include: line-continuation
13 - match: \)|end
14 comment: In an ideal world, command-call-standard would be performing this match because fish highlights the strings which follow as arguments. But we can't do that in a tmLanguage
15 push:
16 - meta_scope: meta.function-call.fish invalid.illegal.function-call.fish
17 - match: '(?=[\s;&)|<>])'
18 pop: true
19 - match: (?=\S)
20 comment: Anonymous scope - Base scope pipeline goes up until one of the definitive ends (newline and ';') or the sequences that could be an end if we're actually inside a $self scope right now (')' and "end")
21 push:
22 - match: \n|(;)|(?=\))|(?=end)
23 captures:
24 1: meta.function-call.operator.fish keyword.operator.control.fish
25 pop: true
26 - match: '(?:[&|]|(?:[0-9]+)?(?:<|>>?|\^\^?))'
27 comment: Match operators ('&', pipe, and redirect) which cannot start a pipeline because they must be consumed within or after a pipeline
28 scope: invalid.illegal.operator.fish
29 - include: comment-internal-end
30 - match: (?=\S)
31 comment: The reason we match '&' here is because we explicitly require it come after a command (unlike ';' which can be alone on a line)
32 push:
33 - match: '(?=[\n;)])|(&)'
34 captures:
35 1: meta.function-call.operator.fish keyword.operator.control.fish
36 pop: true
37 - include: pipeline
38 argument:
39 - match: '(?![\s;&)|<>^])'
40 comment: End arg if it precedes whitespace or operators (excluding stderr redirect '^' due to a fish quirk)
41 push:
42 - match: '(?=[\s;&)|<>])'
43 pop: true
44 - match: \%
45 comment: Process expansion only occurs if the '%' is at the front of the argument, and continues for the entire argument
46 captures:
47 0: meta.string.unquoted.fish punctuation.definition.process.fish
48 push:
49 - meta_scope: meta.parameter.argument.process-expansion.fish
50 - match: '(?=[\s;&)|<>])'
51 pop: true
52 - match: '(?:self|last)(?=$|[\s;&)|<>])'
53 comment: Match special process names. By a convention that I'm making up, scope them as a type of variable
54 scope: meta.string.unquoted.fish variable.language.fish
55 - include: parameter-patterns
56 - match: '(?:[+-]?)(?:[0-9]+\.?[0-9]*|[0-9]*\.?[0-9]+)(?=$|[\s;&)|<>])'
57 comment: Treat a sequence of integers (with possible sign and decimal separator) as a standalone constant. Do this separate to the
58 scope: meta.parameter.argument.numeric.fish meta.string.unquoted.fish constant.numeric.fish
59 - match: '(?![\s($])'
60 comment: This scope can be used by plugins to locate arguments which don't *start* with command substitution or variable expansion and may directly resolve to file paths. Of course, they could have command substitution or variable expansion further on in them, but looking ahead for that too is nontrivial
61 push:
62 - meta_scope: meta.parameter.argument.path.fish
63 - match: '(?=[\s;&)|<>])'
64 pop: true
65 - match: \~
66 comment: Home directory expansion only occurs if the '~' is at the front of the argument, so check it first
67 scope: meta.string.unquoted.fish keyword.operator.tilde.fish
68 - include: parameter-patterns
69 - match: (?!\s)
70 comment: Use standard parameter patterns for whatever doesn't match the above
71 push:
72 - meta_scope: meta.parameter.argument.fish
73 - match: '(?=[\s;&)|<>])'
74 pop: true
75 - include: parameter-patterns
76 command-call-meta:
77 - match: '(builtin|command|exec)\b(?!\s+[-&|])'
78 comment: These meta commands force the parameter to behave as a standard command. They stop at piping
79 captures:
80 1: support.function.fish
81 push:
82 - match: |-
83 (?x)
84 (?# Look ahead for control operations after whitespace)
85 (?=\s*
86 (?:
87 (?# Find simple control operations)
88 [\n;&)]
89 |
90 (?# Find piping)
91 (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
92 )
93 )
94 pop: true
95 - include: line-continuation
96 - include: command-call-standard
97 - match: '(not)\b(?!\s+[-&|])'
98 comment: This meta command acts as a unary operator on the command to the right, which can also be a meta command. It only applies to one command and stops at piping
99 captures:
100 1: keyword.operator.word.fish
101 push:
102 - match: |-
103 (?x)
104 (?# Look ahead for control operations after whitespace)
105 (?=\s*
106 (?:
107 (?# Find simple control operations)
108 [\n;&)]
109 |
110 (?# Find piping)
111 (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
112 )
113 )
114 pop: true
115 - include: line-continuation
116 - include: command-call-meta
117 - include: command-call-standard
118 command-call-standard:
119 - match: '\#'
120 comment: A command call can't be a comment, but this match will only be satisfied if the command is first after a pipe because comments are otherwise consumed earlier
121 push:
122 - meta_scope: invalid.illegal.function-call.fish
123 - match: '(?=[\n)])'
124 pop: true
125 - match: "(?:[&|<>^])"
126 comment: Match an operator which cannot start a command call but does not stop the next characters from being interpreted as a command
127 scope: invalid.illegal.operator.fish
128 - match: (?=\S)
129 comment: Anonymous scope - A complete command comprising a name element and optional parameter, redirection, and comment elements
130 push:
131 - match: |-
132 (?x)
133 (?# Look ahead for operators)
134 (?=
135 (?:
136 (?# Find a control operator)
137 [\n;&)]
138 |
139 (?# Find a pipe operator)
140 (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
141 )
142 )
143 pop: true
144 - match: '(?![\s<>^%])'
145 comment: Anonymous scope - A name or block element. If a block is found, everything up to the `end` command is captured here. Note that redirection and process expansion can't start the element
146 push:
147 - match: '(?=[\s;&)|<>])'
148 pop: true
149 - include: command-call-standard-block
150 - match: '\[(?=[\s<>]|\\\n)'
151 comment: "Look for the alternate form of test, which uses a matching pair of '[' ']'"
152 captures:
153 0: support.function.test.begin.fish
154 push:
155 - match: '(\])|(\n|[;&)|].*)'
156 captures:
157 1: support.function.test.end.fish
158 2: invalid.illegal.function-call.fish
159 pop: true
160 - include: line-continuation
161 - include: parameter
162 - include: redirection
163 - match: '(?:break|continue|return)(?=[\s;&)|<>])'
164 comment: Look for loop/function control commands. We perform no checking on the validity of their scope (because only allowing them in the correct scope won't work if they are used within if-blocks) or parameters (because fish does that during execution not parsing)
165 captures:
166 0: keyword.control.conditional.fish
167 - match: (?!\s)
168 comment: Anonymous scope - A generic name element
169 push:
170 - match: '(?=[\s;&)|<>])'
171 pop: true
172 - match: (?=\()
173 comment: fish would match the whole command name invalid if there was a command substitution anywhere in it, but we can't look ahead that effectively
174 push:
175 - meta_scope: invalid.illegal.function-call.fish
176 - match: '(?=[\s;&)|<>])'
177 pop: true
178 - match: \(
179 push:
180 - match: '\)|(?=[\n;&)|<>])'
181 pop: true
182 - match: (?!\s)
183 comment: Otherwise, treat the element as a fraction of a name made of arbitrary strings (which breaks at an escaped newline)
184 push:
185 - meta_scope: variable.function.fish
186 - match: '(?=[\s;&()|<>])'
187 pop: true
188 - match: \$
189 comment: The string scope explicitly forbids '$' so that the argument rule can pick it up as a variable expansion, but '$' is treated as a literal in command names, so we have to match it separately
190 scope: meta.string.unquoted.fish
191 - include: string
192 - match: \%
193 comment: A command name can't begin with a process expansion operator (however the variable expansion operator '$' is allowed)
194 push:
195 - meta_scope: invalid.illegal.function-call.fish
196 - match: '(?=[\s;&)|<>])'
197 pop: true
198 - include: string
199 - include: redirection
200 - match: '(?:[^\n\S]+)'
201 comment: Match any whitespace characters that aren't the newline
202 push:
203 - match: |-
204 (?x)
205 (?# Look ahead for operators)
206 (?=
207 (?:
208 (?# Find a control operator)
209 [\n;&)]
210 |
211 (?# Find a pipe operator)
212 (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
213 )
214 )
215 pop: true
216 - match: '(?!--[\s;&)|<>])'
217 comment: A list of elements that does not start with an end-of-options parameter
218 push:
219 - match: |-
220 (?x)
221 (?# Look ahead for operators or the end of options)
222 (?=
223 (?:
224 (?# Find a control operator)
225 [\n;&)]
226 |
227 (?# Find a pipe operator)
228 (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
229 |
230 (?# Find a double hyphen)
231 --[\s;&)|<>]
232 )
233 )
234 pop: true
235 - include: line-continuation
236 - include: comment-internal-end
237 - include: redirection
238 - include: parameter
239 - match: '(?=--[\s;&)|<>])'
240 comment: A list of elements that starts with an end-of-options parameter
241 push:
242 - match: |-
243 (?x)
244 (?# Look ahead for operators)
245 (?=
246 (?:
247 (?# Find a control operator)
248 [\n;&)]
249 |
250 (?# Find a pipe operator)
251 (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
252 )
253 )
254 pop: true
255 - match: '(?=--[\s;&)|<>])'
256 comment: Contain just the end-of-options parameter and give it the normal scope
257 push:
258 - match: '(?=[\s;&)|<>])'
259 pop: true
260 - include: parameter
261 - match: (?=\s)
262 comment: A list of elements (now forcibly using arguments)
263 push:
264 - match: |-
265 (?x)
266 (?# Look ahead for operators)
267 (?=
268 (?:
269 (?# Find a control operator)
270 [\n;&)]
271 |
272 (?# Find a pipe operator)
273 (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
274 )
275 )
276 pop: true
277 - include: line-continuation
278 - include: comment-internal-end
279 - include: redirection
280 - include: argument
281 command-call-standard-block:
282 - match: '(begin|while|if|for|switch|function)\s*([&|<>])'
283 comment: Block commands cannot be backgrounded, piped, or redirected
284 captures:
285 1: variable.function.fish
286 2: invalid.illegal.operator.fish
287 - match: (begin)\s*(\))
288 comment: The begin command uniquely cannot be the last command in a command substitution
289 captures:
290 1: variable.function.fish
291 2: invalid.illegal.operator.fish
292 - match: 'begin(?=\s*$|\s*[\n;]|\s+[^\s-])'
293 comment: The begin command can be alone on a line or followed by any command that doesn't start with a '-'. If a '-' is seen it shouldn't be treated as a block
294 captures:
295 0: keyword.control.conditional.fish
296 push:
297 - meta_scope: meta.block.begin.fish
298 - match: 'end(?=$|[\s;&)|<>])'
299 captures:
300 0: keyword.control.conditional.fish
301 pop: true
302 - include: main
303 - match: '(?=while\s+[^\s;)-])'
304 comment: If the command name is followed by a valid string (not anything that closes the scope or a string starting with a '-') then open the scope
305 push:
306 - meta_scope: meta.block.while.fish
307 - match: 'end(?=$|[\s;&)|<>])'
308 captures:
309 0: keyword.control.conditional.fish
310 pop: true
311 - match: while
312 comment: Anonymous scope - Capture the command name we know is there, include a single instance of a pipeline, and end when an operator is seen
313 captures:
314 0: keyword.control.conditional.fish
315 push:
316 - match: '\s*(?=[\n;&)])'
317 pop: true
318 - include: line-continuation
319 - include: pipeline
320 - match: '\n|(;)|([&)])'
321 comment: Anonymous scope - Capture the operator we know is there, include the base scope, and end when an `end` command is seen
322 captures:
323 1: keyword.operator.control.fish
324 2: invalid.illegal.operator.fish
325 push:
326 - match: '(?=end(?:$|[\s;&)|<>]))'
327 pop: true
328 - include: main
329 - match: '(?=if\s+[^\s;)-])'
330 comment: If the command name is followed by a valid string (not anything that closes the scope or a string starting with a '-') then open the scope
331 push:
332 - meta_scope: meta.block.if.fish
333 - match: 'end(?=$|[\s;&)|<>])'
334 captures:
335 0: keyword.control.conditional.fish
336 pop: true
337 - include: command-call-standard-block-if-internal
338 - match: '(?=for\s+[^\s;)-])'
339 comment: If the command name is followed by a valid string (not anything that closes the scope or a string starting with a '-') then open the scope
340 push:
341 - meta_scope: meta.block.for-in.fish
342 - match: 'end(?=$|[\s;&)|<>])'
343 captures:
344 0: keyword.control.conditional.fish
345 pop: true
346 - match: (for)(?:\s+)
347 comment: Anonymous scope - Capture the command name we know is there, include a single instance of a parameter (the varname), and end when the whitespace after the varname is captured
348 captures:
349 1: keyword.control.conditional.fish
350 push:
351 - match: \s+
352 pop: true
353 - include: line-continuation
354 - include: parameter
355 - match: \S+
356 comment: Capture anything that a parameter explicitly rejects, which is mostly operators
357 scope: invalid.illegal.operator.fish
358 - include: line-continuation
359 - match: in(?=\s)
360 comment: Anonymous scope - Capture the command name which might be there, include an arbitrary number of arguments, and end when the control operator is seen
361 captures:
362 0: keyword.control.conditional.fish
363 push:
364 - match: '\s*(?=[\n;&)])'
365 pop: true
366 - include: line-continuation
367 - include: comment-internal-end
368 - include: argument
369 - match: '\n|(;)|([&)])'
370 comment: Anonymous scope - Capture the operator we know is there, include the base scope, and end when an `end` command is seen
371 captures:
372 1: keyword.operator.control.fish
373 2: invalid.illegal.operator.fish
374 push:
375 - match: '(?=end(?:$|[\s;&)|<>]))'
376 pop: true
377 - include: main
378 - match: '\S+?(?=[\s;&)])'
379 comment: Anything beside line continuation, "in", or a control operator is invalid
380 scope: invalid.illegal.function-call.fish
381 - match: '(?=switch\s+[^\s;)-])'
382 comment: If the command name is followed by a valid string (not anything that closes the scope or a string starting with a '-') then open the scope
383 push:
384 - meta_scope: meta.block.switch.fish
385 - match: 'end(?=$|[\s;&)|<>])'
386 captures:
387 0: keyword.control.conditional.fish
388 pop: true
389 - match: (?=switch)
390 comment: Anonymous scope - Match the valid part of the switch statement, then look for an invalid part
391 push:
392 - match: '\s*(?=[\n;&)])'
393 pop: true
394 - match: (switch)(?:\s+)
395 comment: Anonymous scope - Capture the command name we know is there, include a single instance of a parameter (the value), and end when whitespace or a control operator is seen
396 captures:
397 1: keyword.control.conditional.fish
398 push:
399 - match: '(?=[\s;&)])'
400 pop: true
401 - include: line-continuation
402 - include: parameter
403 - match: \S+
404 comment: Capture anything that a parameter explicitly rejects, which is mostly operators
405 scope: invalid.illegal.operator.fish
406 - match: \s+
407 comment: Anonymous scope - Capture whitespace which might be there, match any non-control-operator strings as invalid, and end when a control operator is seen
408 push:
409 - match: '(?=[\n;&)])'
410 pop: true
411 - match: '\S+?(?=[\s;&)])'
412 scope: invalid.illegal.string.fish
413 - match: '\n|(;)|([&)])'
414 comment: Anonymous scope - Capture the operator we know is there, include the base scope, and end when an `end` command is seen
415 captures:
416 1: keyword.operator.control.fish
417 2: invalid.illegal.operator.fish
418 push:
419 - match: '(?=end(?:$|[\s;&)|<>]))'
420 pop: true
421 - match: 'case(?=[\s;&)])'
422 comment: Anonymous scope - Capture the command name which might be there, include an arbitrary number of arguments, and end when the control operator is captured
423 captures:
424 0: keyword.control.conditional.fish
425 push:
426 - match: '\n|(;)|([&)])'
427 captures:
428 1: keyword.operator.control.fish
429 2: invalid.illegal.operator.fish
430 pop: true
431 - include: line-continuation
432 - include: comment-internal-end
433 - include: argument
434 - match: '\S+?(?=[\s;&)])'
435 comment: Anything else (eg, redirection) is illegal
436 scope: invalid.illegal.operator.fish
437 - include: main
438 - match: '(?=function\s+[^\s;)-])'
439 comment: If the command name is followed by a valid string (not anything that closes the scope or a string starting with a '-') then open the scope
440 push:
441 - meta_scope: meta.block.function.fish
442 - match: 'end(?=$|[\s;&)|<>])'
443 captures:
444 0: keyword.control.conditional.fish
445 pop: true
446 - match: (?=function)
447 comment: Anonymous scope - Match the defined name of the function statement, then look for further parameters
448 push:
449 - match: '\s*(?=[\n;&)])'
450 pop: true
451 - match: (function)(?:\s+)
452 comment: Anonymous scope - Capture the command name we know is there, include a single instance of a parameter (the value), and end when whitespace or a control operator is seen
453 captures:
454 1: keyword.control.conditional.fish
455 push:
456 - match: '(?=[\s;&)])'
457 pop: true
458 - include: line-continuation
459 - match: "(?:[()|<>])"
460 push:
461 - meta_scope: invalid.illegal.string.fish
462 - match: '(?=[\s;&)])'
463 pop: true
464 - match: (?!\\\n)
465 comment: Anonymous scope - Start when an escaped newline isn't present, and end when whitespace or an operator is seen
466 push:
467 - match: '(?=[\s;&()|<>])'
468 pop: true
469 - match: (?!\s)
470 push:
471 - meta_scope: entity.name.function.fish
472 - match: '(?=[\s;&()|<>])'
473 pop: true
474 - include: parameter
475 - match: \s+
476 comment: Anonymous scope - Capture whitespace which might be there, then match anything normal for a command call
477 push:
478 - match: '(?=[\n;&)])'
479 pop: true
480 - include: line-continuation
481 - include: comment-internal-end
482 - include: redirection
483 - include: parameter
484 - match: '\n|(;)|([&)])'
485 comment: Anonymous scope - Capture the operator we know is there, include the base scope, and end when an `end` command is seen
486 captures:
487 1: keyword.operator.control.fish
488 2: invalid.illegal.operator.fish
489 push:
490 - match: '(?=end(?:$|[\s;&)|<>]))'
491 pop: true
492 - include: main
493 command-call-standard-block-if-internal:
494 - match: '(?=if(?:\s*\n|\s+[^\s;]))'
495 comment: Anonymous scope - Capture an `if` and the command up to the control operator, then capture from the control operator indefinitely
496 push:
497 - match: '(?=end(?:$|[\s;&)|<>]))'
498 pop: true
499 - match: if
500 comment: Anonymous scope - Match the command name we know is there, include a single instance of a pipeline, and end when a control operator is seen
501 captures:
502 0: keyword.control.conditional.fish
503 push:
504 - match: '\s*(?=[\n;&])'
505 pop: true
506 - include: line-continuation
507 - include: pipeline
508 - match: \n|(;)|(&)
509 comment: Anonymous scope - Match the operator we know is there, then include the base scope or an `else` structure
510 captures:
511 1: keyword.operator.control.fish
512 2: invalid.illegal.operator.fish
513 push:
514 - match: '(?=end(?:$|[\s;&)|<>]))'
515 pop: true
516 - match: '(?=else\s*[\s;])'
517 comment: Anonymous scope - Capture an `else` up to the control operator or the start of an `if` structure, then match from the control operator indefinitely or match an `if` structure
518 push:
519 - match: '(?=end(?:$|[\s;&)|<>]))'
520 pop: true
521 - match: 'else(?=\s*[\s;])'
522 comment: Anonymous scope - Match the `else` we know is there and any comment, and mark anything besides an `if` as illegal
523 captures:
524 0: keyword.control.conditional.fish
525 push:
526 - match: '\s*(?=[\n;&]|if(?:\s*\n|\s+[^\s;]))'
527 pop: true
528 - include: line-continuation
529 - include: comment-internal-end
530 - match: '\S+?(?=[\s;&])'
531 comment: Anything else is illegal
532 scope: invalid.illegal.string.fish
533 - match: \n|(;)|(&)
534 comment: Anonymous scope - Match the operator which will be there if no `if` was seen, then include the base scope which marks further `else` commands as invalid
535 captures:
536 1: keyword.operator.control.fish
537 2: invalid.illegal.operator.fish
538 push:
539 - match: '(?=end(?:$|[\s;&)|<>]))'
540 pop: true
541 - include: main
542 - include: command-call-standard-block-if-internal
543 - include: main
544 command-substitution:
545 - match: (?=\()
546 comment: 'Capture "(...)" or "(...)[...]"'
547 push:
548 - match: '(?![\(\[])'
549 pop: true
550 - match: \(
551 captures:
552 0: punctuation.section.parens.begin.fish
553 push:
554 - meta_scope: meta.parens.command-substitution.fish
555 - match: \)
556 captures:
557 0: punctuation.section.parens.end.fish
558 pop: true
559 - include: main
560 - include: index-expansion
561 comment-external:
562 - match: '\#'
563 comment: A full or inline comment outside of any command call
564 captures:
565 0: punctuation.definition.comment.fish
566 push:
567 - meta_scope: comment.line.external.fish
568 - match: \n
569 pop: true
570 comment-internal-end:
571 - match: '\#'
572 comment: An inline comment at the end of a command call. Does not consume the newline, thus allowing the command call to capture it and end
573 captures:
574 0: punctuation.definition.comment.fish
575 push:
576 - meta_scope: comment.line.internal.end.fish
577 - match: (?=\n)
578 pop: true
579 index-expansion:
580 - match: '\['
581 comment: In other words, the anonymous scope which contains the variable and the index expansion parameter list should only be allowed to contain a single copy of each of those two things. We cannot enforce that without a scope stack. Our workaround is to allow an infinite number of these and hope the user can keep track of when there are too many
582 captures:
583 0: punctuation.section.brackets.begin.fish
584 push:
585 - meta_scope: meta.brackets.index-expansion.fish
586 - match: '\]'
587 captures:
588 0: punctuation.section.brackets.end.fish
589 pop: true
590 - match: \.\.
591 scope: keyword.operator.range.fish
592 - include: command-substitution
593 - include: variable-expansion
594 - include: string-quoted
595 - match: '(?:[+-]?[0-9]+)(?=[\s;&)|<>]|\]|\.\.)'
596 scope: constant.numeric.fish
597 - match: '(?![\s''"]|\.\.)'
598 comment: 'Begin/end string as before with the addition of breaking at a '']'' or ".."'
599 push:
600 - match: '(?=[\s;&)|<>''"]|\]|\.\.)'
601 pop: true
602 - include: string-unquoted-patterns
603 line-continuation:
604 - match: (?=\\\n)
605 comment: End when an unescaped newline is seen, the first character of a line isn't whitespace or a comment character or the escaped newline itself, or if the next character after some consumed whitespace isn't more whitespace or a comment character
606 push:
607 - match: '(?=\n)|^(?![\s\#\\])|\s(?![\s\#])'
608 pop: true
609 - match: \\\n
610 scope: constant.character.escape
611 - match: '\#'
612 captures:
613 0: punctuation.definition.comment.fish
614 push:
615 - meta_scope: comment.line.continuation.fish
616 - match: \n
617 pop: true
618 parameter:
619 - match: '(?![\s;&)|<>^])'
620 comment: See the argument rule for more general information on parameters
621 push:
622 - match: '(?=[\s;&)|<>])'
623 pop: true
624 - match: '(?:--)(?=[\s;&)|<>])'
625 comment: End of options (parameter of just two hyphens)
626 scope: meta.parameter.option.end.fish variable.parameter.fish punctuation.definition.option.end.fish meta.string.unquoted.fish
627 - match: (?=--)
628 comment: Long option (parameter starting with two hyphens)
629 push:
630 - meta_scope: meta.parameter.option.long.fish
631 - match: '(?=[\s;&)|<>])'
632 pop: true
633 - match: (?:--)
634 captures:
635 0: punctuation.definition.option.long.begin.fish meta.string.unquoted.fish
636 push:
637 - meta_scope: variable.parameter.fish
638 - match: '(?=[\s;&)|<>]|=)'
639 pop: true
640 - include: command-substitution
641 - match: (?=\$)
642 push:
643 - meta_scope: meta.string.unquoted.fish
644 - match: (?!\$)
645 pop: true
646 - include: variable-expansion
647 - include: string-quoted
648 - match: '(?![''"])'
649 push:
650 - meta_scope: meta.string.unquoted.fish
651 - match: '(?=[\s;&()|<>''"$]|\=)'
652 pop: true
653 - include: string-unquoted-patterns
654 - match: (?:=)
655 comment: Consume the '=' and then use standard parameter patterns as well as numerics
656 captures:
657 0: variable.parameter.fish punctuation.definition.option.long.separator.fish meta.string.unquoted.fish
658 push:
659 - match: '(?=[\s;&)|<>])'
660 pop: true
661 - match: '(?:[+-]?)(?:[0-9]+\.?[0-9]*|[0-9]*\.?[0-9]+)(?=$|[\s;&)|<>])'
662 scope: meta.string.unquoted.fish constant.numeric.fish
663 - include: parameter-patterns
664 - match: '(?:-)(?=[^\s;&)|<>])'
665 comment: Short option (parameter starting with one hyphen)
666 captures:
667 0: punctuation.definition.option.short.fish meta.string.unquoted.fish
668 push:
669 - meta_scope: meta.parameter.option.short.fish variable.parameter.fish
670 - match: '(?=[\s;&)|<>])'
671 pop: true
672 - include: parameter-patterns
673 - include: argument
674 parameter-patterns:
675 - include: command-substitution
676 - match: (?=\$)
677 comment: Give variable expansion the unquoted string scope
678 push:
679 - meta_scope: meta.string.unquoted.fish
680 - match: (?!\$)
681 pop: true
682 - include: variable-expansion
683 - include: string
684 pipeline:
685 - match: '(?:[&|]|(?:[0-9]+)?(?:<|>>?|\^\^?))'
686 comment: Todo - Restructure pipeline so that this match isn't duplicated from the base scope, which it must also be for any other scopes which implement their own control operator consumption. Might require the unary operator commands to become an explicit recursive match (though we tried this once and it was more complicated than anything should be)
687 scope: invalid.illegal.operator.fish
688 - match: (and|or)\b(?!\s+-)
689 comment: Todo - These commands cannot be followed by backgrounding, piping, or redirection alone. Add logic to catch these cases. It will be extensive...
690 scope: meta.function-call.fish keyword.operator.word.fish
691 - include: line-continuation
692 - match: (not)\b(?!\s+-)
693 comment: This is a hack for now, which allows nesting of 'not' and 'and'/'or' commands. A better solution will be explicit recursivity in these commands
694 scope: meta.function-call.fish keyword.operator.word.fish
695 - match: '(?:case|else|end)(?=[\s;&)|<>])'
696 comment: Match a command which is illegal in the base scope
697 scope: invalid.illegal.function-call.fish
698 - match: '(?=[^\s#])'
699 comment: Anonymous scope - Pipeline. Define a pipeline as either one command call, or multiple command calls linked by pipe operators ('|', '2>|', etc). The pipeline terminates at the first encounter of any control operator
700 push:
701 - match: '(\s*)(?=[\n;&)])'
702 captures:
703 1: meta.function-call.fish
704 pop: true
705 - match: |-
706 (?x)
707 (?# Negative lookahead for piping)
708 (?!
709 (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
710 )
711 comment: Match the first command of a potential pipeline
712 push:
713 - meta_scope: meta.function-call.fish
714 - match: |-
715 (?x)
716 (?# Look ahead for operators after whitespace)
717 (?=\s*
718 (?:
719 (?# Find a control operator)
720 [\n;&)]
721 |
722 (?# Find a pipe operator)
723 (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
724 )
725 )
726 pop: true
727 - include: command-call-meta
728 - include: command-call-standard
729 - match: |-
730 (?x)
731 (?# Look ahead for piping)
732 (?=
733 (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
734 )
735 comment: Match a second or later command of a pipeline, starting with the connective piping
736 push:
737 - meta_scope: meta.function-call.fish
738 - match: '(?=\s*[\n;&)])'
739 pop: true
740 - match: |-
741 (?x)
742 (?# Look ahead for piping followed by either control operators or piping)
743 (?=
744 (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
745 \s*
746 (?:
747 $
748 |
749 [\n;&)]
750 |
751 (?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
752 )
753 )
754 comment: Match a pipe not followed by a command, hence a malformed segment of the pipeline
755 push:
756 - match: '(?=\s*$|\s*[\n;&)])'
757 pop: true
758 - match: '(?:(?:[0-9]+)?(?:<|>|>>))?\|(?=\s*$|\s*[\n)])'
759 comment: If the pipeline would end implicitly (ie, with a newline or close parenthesis), then mark the pipe itself invalid
760 scope: invalid.illegal.operator.fish
761 - match: |-
762 (?x)
763 (?# Consume valid piping; captures 1 2 3)
764 (?:([0-9]+)?(<|>>?|\^\^?))?(\|)
765 (?# Consume whitespace)
766 \s*
767 (?# Consume remainder; capture 4)
768 (.*)
769 comment: If the pipeline would end with an explicit operator or encounter a second set of piping, then mark the first set of piping as valid and beyond as invalid
770 captures:
771 1: meta.pipe.fish constant.numeric.file-descriptor.fish
772 2: meta.pipe.fish keyword.operator.redirect.fish
773 3: meta.pipe.fish keyword.operator.pipe.fish
774 4: invalid.illegal.function-call.fish
775 - match: '(?:([0-9]+)?(<|>>?|\^\^?))?(\|)'
776 comment: Pick up a legitimate pipe
777 scope: meta.pipe.fish
778 captures:
779 1: constant.numeric.file-descriptor.fish
780 2: keyword.operator.pipe.redirect.fish
781 3: keyword.operator.pipe.fish
782 - include: line-continuation
783 - include: command-call-meta
784 - include: command-call-standard
785 redirection:
786 - match: '(?=(?:[0-9]+(?:<|>|>>)|>>|\^\^|[<>^])\&)'
787 comment: End at anything that would end a parameter, including redirections *if* they are *not* this same type of redirection (ie, have an '&'), in which case this scope stays open and we match the next one. The negative lookahead for <>^ at the end is to keep ST2 happy (not hanging)
788 push:
789 - match: '(?=[\s;&)|]|(?:[0-9]+(?:<|>|>>)|>>|\^\^|[<>^])(?![&<>^]))'
790 pop: true
791 - match: '(?:([0-9]+)(<|>|>>)|(>>|\^\^|[<>^]))(\&)\s*'
792 comment: We have to try and catch an '&' here because if it is seen by the outer end match then it will be considered a valid operator and the redirection scope will immediately terminate
793 captures:
794 1: constant.numeric.file-descriptor.fish
795 2: keyword.operator.redirect.fish
796 3: keyword.operator.redirect.fish
797 4: keyword.operator.redirect.dereference.fish
798 push:
799 - meta_scope: meta.redirection.fish
800 - match: '(\&.*$)|(?![&\\])'
801 captures:
802 1: invalid.illegal.file-descriptor.fish
803 pop: true
804 - include: line-continuation
805 - match: (?=\\\n)
806 push:
807 - meta_scope: meta.redirection.fish
808 - match: (?!\\\n)
809 pop: true
810 - include: line-continuation
811 - match: (?=\()
812 comment: Evaluates to a string which may be an integer
813 push:
814 - meta_scope: meta.redirection.fish
815 - match: (?!\()
816 pop: true
817 - include: command-substitution
818 - match: (?=\$)
819 comment: Evaluates to a string which may be an integer
820 push:
821 - meta_scope: meta.redirection.fish
822 - match: (?!\$)
823 pop: true
824 - include: variable-expansion
825 - match: '(?=[''"])'
826 comment: May be a quoted integer, which is allowed
827 push:
828 - meta_scope: meta.redirection.fish
829 - match: '(?![''"])'
830 pop: true
831 - include: string-quoted
832 - match: '(?:[0-9]+)(?=$|[\s;&)|<>])'
833 scope: meta.redirection.file-descriptor.fish constant.numeric.file-descriptor.fish
834 - match: '(?:-)(?=$|[\s;&)|<>])'
835 scope: meta.redirection.file-descriptor.fish keyword.operator.redirect.close.fish
836 - match: (?:\S+.*)$
837 comment: Anything else is illegal
838 scope: meta.redirection.fish invalid.illegal.file-descriptor.fish
839 - match: '(?=(?:[0-9]+(?:<|>|>>)|>>|\^\^|[<>^])\??)'
840 comment: End at anything that would end a parameter, including redirections *if* they are *not* this same type of redirection (ie, redirection into file descriptor, or into pipe), in which case this scope stays open and we match the next one
841 push:
842 - match: '(?=[\s;&)|]|(?:[0-9]+(?:<|>|>>)|>>|\^\^|[<>^])[&|])'
843 pop: true
844 - match: '(?:([0-9]+)(<|>|>>)|(>>|\^\^|[<>^]))(\?)?\s*'
845 comment: We have to try and catch bad operators here because if they are seen by the outer end match then they will be considered valid and the redirection scope will immediately terminate
846 captures:
847 1: constant.numeric.file-descriptor.fish
848 2: keyword.operator.redirect.fish
849 3: keyword.operator.redirect.fish
850 4: keyword.operator.redirect.clobber-test.fish
851 push:
852 - meta_scope: meta.redirection.fish
853 - match: "((?:[&?]|[0-9]*[<>^]).*$)|(?![&?<>^])"
854 captures:
855 1: invalid.illegal.path.fish
856 pop: true
857 - include: line-continuation
858 - match: (?=\\\n)
859 push:
860 - meta_scope: meta.redirection.fish
861 - match: (?!\\\n)
862 pop: true
863 - include: line-continuation
864 - match: (?=\()
865 comment: Evaluates to a string, so path cannot begin with '('
866 push:
867 - meta_scope: meta.redirection.fish
868 - match: (?!\()
869 pop: true
870 - include: command-substitution
871 - match: (?=\$)
872 comment: Evaluates to a string, so path cannot begin with '$'
873 push:
874 - meta_scope: meta.redirection.fish
875 - match: (?!\$)
876 pop: true
877 - include: variable-expansion
878 - match: "(?:[&?]|[0-9]*[<>^]).*$"
879 comment: Check for characters which are associated with redirection, so path cannot begin with them. Don't put them in the meta.redirection.path scope, so that only valid paths are in there
880 scope: meta.redirection.fish invalid.illegal.path.fish
881 - match: \~
882 scope: meta.redirection.path.fish keyword.operator.tilde.fish
883 - match: '(?:\''(?:\\[\''\\]|[^\''\\])*\''|\"(?:\\[\"$\n\\]|[^\"$\n\\])*\"|(?:\\[abefnrtv $\\*?#(){}\[\]<>^&;|"'']|\\[~%]|\\[xX][0-9A-Fa-f]{1,2}|\\[0-7]{1,3}|\\u[0-9A-Fa-f]{1,4}|\\U[0-9A-Fa-f]{1,8}|\\c[?-~]|[^\s$\\*?~%#()<>&|;"'']|\\(?=[^abefnrtv\s$\\*?#(){}\[\]<>^&;|"''xXuUc])|\\\n|[~%#])+)+'
884 comment: Use the function call match to build a file path, as the syntax is fairly similar (possibly identical, after exceptions caught above? I haven't checked)
885 scope: meta.redirection.path.fish
886 string:
887 - include: string-quoted
888 - include: string-unquoted
889 string-quoted:
890 - match: \'
891 captures:
892 0: punctuation.definition.string.begin.fish
893 push:
894 - meta_scope: string.quoted.single.fish
895 - match: \'
896 captures:
897 0: punctuation.definition.string.end.fish
898 pop: true
899 - match: '\\[\''\\]'
900 comment: Only accepted escapes are \' and \\
901 scope: constant.character.escape.fish
902 - match: \"
903 captures:
904 0: punctuation.definition.string.begin.fish
905 push:
906 - meta_scope: string.quoted.double.fish
907 - match: \"
908 captures:
909 0: punctuation.definition.string.end.fish
910 pop: true
911 - match: '\\[\n\"\\$]'
912 comment: Only accepted escapes are \<newline>, \", \\, and \$
913 scope: constant.character.escape.fish
914 - include: variable-expansion
915 string-unquoted:
916 - match: '(?![\s;&()|<>''"$])'
917 comment: End unquoted string at anything that can't be in one
918 push:
919 - meta_scope: meta.string.unquoted.fish
920 - match: '(?=[\s;&()|<>''"$])'
921 pop: true
922 - include: string-unquoted-patterns
923 string-unquoted-patterns:
924 - match: |-
925 (?x)
926 \\[abefnrtv $\\*?#(){}\[\]<>^&|;"']
927 |
928 \\[~%]
929 |
930 \\[xX][0-9A-Fa-f]{1,2}
931 |
932 \\[0-7]{1,3}
933 |
934 \\u[0-9A-Fa-f]{1,4}
935 |
936 \\U[0-9A-Fa-f]{1,8}
937 |
938 \\c[?-~]
939 comment: This list follows the order given in official fish documentation. Technically '~' and '%' only need escaping if they appear at the front of a parameter. If they are escaped within a parameter, then fish does not *highlight* the escape, however it does silently *parse* the escape and the backslash is removed before the parameter is passed to the command. So, we highlight these escapes as well since they are actually treated as valid escapes by fish
940 scope: constant.character.escape.fish
941 - match: \\\n
942 comment: Just for convenience we separate the newline escape
943 scope: constant.character.escape.fish
944 - match: '\{'
945 captures:
946 0: punctuation.section.braces.begin.fish
947 push:
948 - meta_scope: meta.braces.brace-expansion.fish
949 - match: '(\})|(\n|[;&)|].*)'
950 captures:
951 1: punctuation.section.braces.end.fish
952 2: invalid.illegal.punctuation.section.fish
953 pop: true
954 - match: \,
955 scope: punctuation.section.braces.separator.fish
956 - include: command-substitution
957 - include: variable-expansion
958 - match: '(?:[^\S\n]+)'
959 comment: Unescaped spaces aren't allowed, as technically that separates the braces into two separate arguments. Don't consume a newline though, so the scope end capture can get it
960 scope: invalid.illegal.whitespace.fish
961 - include: string-quoted
962 - match: '(?:[+-]?)(?:[0-9]+\.?[0-9]*|[0-9]*\.?[0-9]+)(?=$|[\s;&)|<>]|\}|\,)'
963 scope: constant.numeric.fish
964 - match: '(?![\s;&)|<>''"])'
965 comment: "Begin/end string as before with the addition of breaking at a '}' or ','"
966 push:
967 - match: '(?=[\s;&)|<>''"]|\}|\,)'
968 pop: true
969 - match: \\\,
970 scope: constant.character.escape.fish
971 - include: string-unquoted-patterns
972 - match: (\*\*)|(\*)|(\?)
973 scope: meta.wildcard-expansion.fish
974 captures:
975 1: keyword.operator.double-star.fish
976 2: keyword.operator.single-star.fish
977 3: keyword.operator.question-mark.fish
978 variable-expansion:
979 - include: variable-expansion-illegal
980 - match: (?=\$)
981 comment: 'Capture "$foo" or "$foo[]" or "$$foo[][]" etc'
982 push:
983 - meta_scope: meta.variable-expansion.fish
984 - match: '(?=[^\$\w\[])'
985 pop: true
986 - match: \$
987 captures:
988 0: punctuation.definition.variable.fish
989 push:
990 - meta_scope: variable.other.fish
991 - match: '(?=[^\$\w])'
992 pop: true
993 - include: variable-expansion-illegal
994 - include: variable-expansion-simple
995 - include: index-expansion
996 variable-expansion-illegal:
997 - match: '\$(?:(?=[,''"\]}\s;&)|])|[^\w\$][^$,''"\]}\s;&)|]*)'
998 comment: A lone '$' in a scope, or an attempt to expand a variable starting with a nonword character, is an error. These boundaries are the same as for meta.string.unquoted
999 scope: invalid.illegal.variable-expansion.fish
1000 variable-expansion-simple:
1001 - match: \$
1002 captures:
1003 0: punctuation.definition.variable.fish
1004 push:
1005 - meta_scope: variable.other.fish
1006 - match: '(?=[^\$\w])'
1007 pop: true
1008 - include: variable-expansion-illegal
1009 - include: variable-expansion-simple