bat/assets/syntaxes/fish.sublime-syntax
2019-04-25 17:52:21 +02:00

1010 lines
45 KiB
YAML
Vendored

%YAML 1.2
---
# http://www.sublimetext.com/docs/3/syntax.html
name: friendly interactive shell (fish)
file_extensions:
- fish
first_line_match: '^#!.*\b(fish)|^#\s*-\*-[^*]*mode:\s*shell-script[^*]*-\*-'
scope: source.shell.fish
contexts:
main:
- include: comment-external
- include: line-continuation
- match: \)|end
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
push:
- meta_scope: meta.function-call.fish invalid.illegal.function-call.fish
- match: '(?=[\s;&)|<>])'
pop: true
- match: (?=\S)
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")
push:
- match: \n|(;)|(?=\))|(?=end)
captures:
1: meta.function-call.operator.fish keyword.operator.control.fish
pop: true
- match: '(?:[&|]|(?:[0-9]+)?(?:<|>>?|\^\^?))'
comment: Match operators ('&', pipe, and redirect) which cannot start a pipeline because they must be consumed within or after a pipeline
scope: invalid.illegal.operator.fish
- include: comment-internal-end
- match: (?=\S)
comment: The reason we match '&' here is because we explicitly require it come after a command (unlike ';' which can be alone on a line)
push:
- match: '(?=[\n;)])|(&)'
captures:
1: meta.function-call.operator.fish keyword.operator.control.fish
pop: true
- include: pipeline
argument:
- match: '(?![\s;&)|<>^])'
comment: End arg if it precedes whitespace or operators (excluding stderr redirect '^' due to a fish quirk)
push:
- match: '(?=[\s;&)|<>])'
pop: true
- match: \%
comment: Process expansion only occurs if the '%' is at the front of the argument, and continues for the entire argument
captures:
0: meta.string.unquoted.fish punctuation.definition.process.fish
push:
- meta_scope: meta.parameter.argument.process-expansion.fish
- match: '(?=[\s;&)|<>])'
pop: true
- match: '(?:self|last)(?=$|[\s;&)|<>])'
comment: Match special process names. By a convention that I'm making up, scope them as a type of variable
scope: meta.string.unquoted.fish variable.language.fish
- include: parameter-patterns
- match: '(?:[+-]?)(?:[0-9]+\.?[0-9]*|[0-9]*\.?[0-9]+)(?=$|[\s;&)|<>])'
comment: Treat a sequence of integers (with possible sign and decimal separator) as a standalone constant. Do this separate to the
scope: meta.parameter.argument.numeric.fish meta.string.unquoted.fish constant.numeric.fish
- match: '(?![\s($])'
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
push:
- meta_scope: meta.parameter.argument.path.fish
- match: '(?=[\s;&)|<>])'
pop: true
- match: \~
comment: Home directory expansion only occurs if the '~' is at the front of the argument, so check it first
scope: meta.string.unquoted.fish keyword.operator.tilde.fish
- include: parameter-patterns
- match: (?!\s)
comment: Use standard parameter patterns for whatever doesn't match the above
push:
- meta_scope: meta.parameter.argument.fish
- match: '(?=[\s;&)|<>])'
pop: true
- include: parameter-patterns
command-call-meta:
- match: '(builtin|command|exec)\b(?!\s+[-&|])'
comment: These meta commands force the parameter to behave as a standard command. They stop at piping
captures:
1: support.function.fish
push:
- match: |-
(?x)
(?# Look ahead for control operations after whitespace)
(?=\s*
(?:
(?# Find simple control operations)
[\n;&)]
|
(?# Find piping)
(?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
)
)
pop: true
- include: line-continuation
- include: command-call-standard
- match: '(not)\b(?!\s+[-&|])'
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
captures:
1: keyword.operator.word.fish
push:
- match: |-
(?x)
(?# Look ahead for control operations after whitespace)
(?=\s*
(?:
(?# Find simple control operations)
[\n;&)]
|
(?# Find piping)
(?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
)
)
pop: true
- include: line-continuation
- include: command-call-meta
- include: command-call-standard
command-call-standard:
- match: '\#'
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
push:
- meta_scope: invalid.illegal.function-call.fish
- match: '(?=[\n)])'
pop: true
- match: "(?:[&|<>^])"
comment: Match an operator which cannot start a command call but does not stop the next characters from being interpreted as a command
scope: invalid.illegal.operator.fish
- match: (?=\S)
comment: Anonymous scope - A complete command comprising a name element and optional parameter, redirection, and comment elements
push:
- match: |-
(?x)
(?# Look ahead for operators)
(?=
(?:
(?# Find a control operator)
[\n;&)]
|
(?# Find a pipe operator)
(?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
)
)
pop: true
- match: '(?![\s<>^%])'
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
push:
- match: '(?=[\s;&)|<>])'
pop: true
- include: command-call-standard-block
- match: '\[(?=[\s<>]|\\\n)'
comment: "Look for the alternate form of test, which uses a matching pair of '[' ']'"
captures:
0: support.function.test.begin.fish
push:
- match: '(\])|(\n|[;&)|].*)'
captures:
1: support.function.test.end.fish
2: invalid.illegal.function-call.fish
pop: true
- include: line-continuation
- include: parameter
- include: redirection
- match: '(?:break|continue|return)(?=[\s;&)|<>])'
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)
captures:
0: keyword.control.conditional.fish
- match: (?!\s)
comment: Anonymous scope - A generic name element
push:
- match: '(?=[\s;&)|<>])'
pop: true
- match: (?=\()
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
push:
- meta_scope: invalid.illegal.function-call.fish
- match: '(?=[\s;&)|<>])'
pop: true
- match: \(
push:
- match: '\)|(?=[\n;&)|<>])'
pop: true
- match: (?!\s)
comment: Otherwise, treat the element as a fraction of a name made of arbitrary strings (which breaks at an escaped newline)
push:
- meta_scope: variable.function.fish
- match: '(?=[\s;&()|<>])'
pop: true
- match: \$
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
scope: meta.string.unquoted.fish
- include: string
- match: \%
comment: A command name can't begin with a process expansion operator (however the variable expansion operator '$' is allowed)
push:
- meta_scope: invalid.illegal.function-call.fish
- match: '(?=[\s;&)|<>])'
pop: true
- include: string
- include: redirection
- match: '(?:[^\n\S]+)'
comment: Match any whitespace characters that aren't the newline
push:
- match: |-
(?x)
(?# Look ahead for operators)
(?=
(?:
(?# Find a control operator)
[\n;&)]
|
(?# Find a pipe operator)
(?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
)
)
pop: true
- match: '(?!--[\s;&)|<>])'
comment: A list of elements that does not start with an end-of-options parameter
push:
- match: |-
(?x)
(?# Look ahead for operators or the end of options)
(?=
(?:
(?# Find a control operator)
[\n;&)]
|
(?# Find a pipe operator)
(?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
|
(?# Find a double hyphen)
--[\s;&)|<>]
)
)
pop: true
- include: line-continuation
- include: comment-internal-end
- include: redirection
- include: parameter
- match: '(?=--[\s;&)|<>])'
comment: A list of elements that starts with an end-of-options parameter
push:
- match: |-
(?x)
(?# Look ahead for operators)
(?=
(?:
(?# Find a control operator)
[\n;&)]
|
(?# Find a pipe operator)
(?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
)
)
pop: true
- match: '(?=--[\s;&)|<>])'
comment: Contain just the end-of-options parameter and give it the normal scope
push:
- match: '(?=[\s;&)|<>])'
pop: true
- include: parameter
- match: (?=\s)
comment: A list of elements (now forcibly using arguments)
push:
- match: |-
(?x)
(?# Look ahead for operators)
(?=
(?:
(?# Find a control operator)
[\n;&)]
|
(?# Find a pipe operator)
(?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
)
)
pop: true
- include: line-continuation
- include: comment-internal-end
- include: redirection
- include: argument
command-call-standard-block:
- match: '(begin|while|if|for|switch|function)\s*([&|<>])'
comment: Block commands cannot be backgrounded, piped, or redirected
captures:
1: variable.function.fish
2: invalid.illegal.operator.fish
- match: (begin)\s*(\))
comment: The begin command uniquely cannot be the last command in a command substitution
captures:
1: variable.function.fish
2: invalid.illegal.operator.fish
- match: 'begin(?=\s*$|\s*[\n;]|\s+[^\s-])'
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
captures:
0: keyword.control.conditional.fish
push:
- meta_scope: meta.block.begin.fish
- match: 'end(?=$|[\s;&)|<>])'
captures:
0: keyword.control.conditional.fish
pop: true
- include: main
- match: '(?=while\s+[^\s;)-])'
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
push:
- meta_scope: meta.block.while.fish
- match: 'end(?=$|[\s;&)|<>])'
captures:
0: keyword.control.conditional.fish
pop: true
- match: while
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
captures:
0: keyword.control.conditional.fish
push:
- match: '\s*(?=[\n;&)])'
pop: true
- include: line-continuation
- include: pipeline
- match: '\n|(;)|([&)])'
comment: Anonymous scope - Capture the operator we know is there, include the base scope, and end when an `end` command is seen
captures:
1: keyword.operator.control.fish
2: invalid.illegal.operator.fish
push:
- match: '(?=end(?:$|[\s;&)|<>]))'
pop: true
- include: main
- match: '(?=if\s+[^\s;)-])'
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
push:
- meta_scope: meta.block.if.fish
- match: 'end(?=$|[\s;&)|<>])'
captures:
0: keyword.control.conditional.fish
pop: true
- include: command-call-standard-block-if-internal
- match: '(?=for\s+[^\s;)-])'
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
push:
- meta_scope: meta.block.for-in.fish
- match: 'end(?=$|[\s;&)|<>])'
captures:
0: keyword.control.conditional.fish
pop: true
- match: (for)(?:\s+)
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
captures:
1: keyword.control.conditional.fish
push:
- match: \s+
pop: true
- include: line-continuation
- include: parameter
- match: \S+
comment: Capture anything that a parameter explicitly rejects, which is mostly operators
scope: invalid.illegal.operator.fish
- include: line-continuation
- match: in(?=\s)
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
captures:
0: keyword.control.conditional.fish
push:
- match: '\s*(?=[\n;&)])'
pop: true
- include: line-continuation
- include: comment-internal-end
- include: argument
- match: '\n|(;)|([&)])'
comment: Anonymous scope - Capture the operator we know is there, include the base scope, and end when an `end` command is seen
captures:
1: keyword.operator.control.fish
2: invalid.illegal.operator.fish
push:
- match: '(?=end(?:$|[\s;&)|<>]))'
pop: true
- include: main
- match: '\S+?(?=[\s;&)])'
comment: Anything beside line continuation, "in", or a control operator is invalid
scope: invalid.illegal.function-call.fish
- match: '(?=switch\s+[^\s;)-])'
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
push:
- meta_scope: meta.block.switch.fish
- match: 'end(?=$|[\s;&)|<>])'
captures:
0: keyword.control.conditional.fish
pop: true
- match: (?=switch)
comment: Anonymous scope - Match the valid part of the switch statement, then look for an invalid part
push:
- match: '\s*(?=[\n;&)])'
pop: true
- match: (switch)(?:\s+)
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
captures:
1: keyword.control.conditional.fish
push:
- match: '(?=[\s;&)])'
pop: true
- include: line-continuation
- include: parameter
- match: \S+
comment: Capture anything that a parameter explicitly rejects, which is mostly operators
scope: invalid.illegal.operator.fish
- match: \s+
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
push:
- match: '(?=[\n;&)])'
pop: true
- match: '\S+?(?=[\s;&)])'
scope: invalid.illegal.string.fish
- match: '\n|(;)|([&)])'
comment: Anonymous scope - Capture the operator we know is there, include the base scope, and end when an `end` command is seen
captures:
1: keyword.operator.control.fish
2: invalid.illegal.operator.fish
push:
- match: '(?=end(?:$|[\s;&)|<>]))'
pop: true
- match: 'case(?=[\s;&)])'
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
captures:
0: keyword.control.conditional.fish
push:
- match: '\n|(;)|([&)])'
captures:
1: keyword.operator.control.fish
2: invalid.illegal.operator.fish
pop: true
- include: line-continuation
- include: comment-internal-end
- include: argument
- match: '\S+?(?=[\s;&)])'
comment: Anything else (eg, redirection) is illegal
scope: invalid.illegal.operator.fish
- include: main
- match: '(?=function\s+[^\s;)-])'
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
push:
- meta_scope: meta.block.function.fish
- match: 'end(?=$|[\s;&)|<>])'
captures:
0: keyword.control.conditional.fish
pop: true
- match: (?=function)
comment: Anonymous scope - Match the defined name of the function statement, then look for further parameters
push:
- match: '\s*(?=[\n;&)])'
pop: true
- match: (function)(?:\s+)
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
captures:
1: keyword.control.conditional.fish
push:
- match: '(?=[\s;&)])'
pop: true
- include: line-continuation
- match: "(?:[()|<>])"
push:
- meta_scope: invalid.illegal.string.fish
- match: '(?=[\s;&)])'
pop: true
- match: (?!\\\n)
comment: Anonymous scope - Start when an escaped newline isn't present, and end when whitespace or an operator is seen
push:
- match: '(?=[\s;&()|<>])'
pop: true
- match: (?!\s)
push:
- meta_scope: entity.name.function.fish
- match: '(?=[\s;&()|<>])'
pop: true
- include: parameter
- match: \s+
comment: Anonymous scope - Capture whitespace which might be there, then match anything normal for a command call
push:
- match: '(?=[\n;&)])'
pop: true
- include: line-continuation
- include: comment-internal-end
- include: redirection
- include: parameter
- match: '\n|(;)|([&)])'
comment: Anonymous scope - Capture the operator we know is there, include the base scope, and end when an `end` command is seen
captures:
1: keyword.operator.control.fish
2: invalid.illegal.operator.fish
push:
- match: '(?=end(?:$|[\s;&)|<>]))'
pop: true
- include: main
command-call-standard-block-if-internal:
- match: '(?=if(?:\s*\n|\s+[^\s;]))'
comment: Anonymous scope - Capture an `if` and the command up to the control operator, then capture from the control operator indefinitely
push:
- match: '(?=end(?:$|[\s;&)|<>]))'
pop: true
- match: if
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
captures:
0: keyword.control.conditional.fish
push:
- match: '\s*(?=[\n;&])'
pop: true
- include: line-continuation
- include: pipeline
- match: \n|(;)|(&)
comment: Anonymous scope - Match the operator we know is there, then include the base scope or an `else` structure
captures:
1: keyword.operator.control.fish
2: invalid.illegal.operator.fish
push:
- match: '(?=end(?:$|[\s;&)|<>]))'
pop: true
- match: '(?=else\s*[\s;])'
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
push:
- match: '(?=end(?:$|[\s;&)|<>]))'
pop: true
- match: 'else(?=\s*[\s;])'
comment: Anonymous scope - Match the `else` we know is there and any comment, and mark anything besides an `if` as illegal
captures:
0: keyword.control.conditional.fish
push:
- match: '\s*(?=[\n;&]|if(?:\s*\n|\s+[^\s;]))'
pop: true
- include: line-continuation
- include: comment-internal-end
- match: '\S+?(?=[\s;&])'
comment: Anything else is illegal
scope: invalid.illegal.string.fish
- match: \n|(;)|(&)
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
captures:
1: keyword.operator.control.fish
2: invalid.illegal.operator.fish
push:
- match: '(?=end(?:$|[\s;&)|<>]))'
pop: true
- include: main
- include: command-call-standard-block-if-internal
- include: main
command-substitution:
- match: (?=\()
comment: 'Capture "(...)" or "(...)[...]"'
push:
- match: '(?![\(\[])'
pop: true
- match: \(
captures:
0: punctuation.section.parens.begin.fish
push:
- meta_scope: meta.parens.command-substitution.fish
- match: \)
captures:
0: punctuation.section.parens.end.fish
pop: true
- include: main
- include: index-expansion
comment-external:
- match: '\#'
comment: A full or inline comment outside of any command call
captures:
0: punctuation.definition.comment.fish
push:
- meta_scope: comment.line.external.fish
- match: \n
pop: true
comment-internal-end:
- match: '\#'
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
captures:
0: punctuation.definition.comment.fish
push:
- meta_scope: comment.line.internal.end.fish
- match: (?=\n)
pop: true
index-expansion:
- match: '\['
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
captures:
0: punctuation.section.brackets.begin.fish
push:
- meta_scope: meta.brackets.index-expansion.fish
- match: '\]'
captures:
0: punctuation.section.brackets.end.fish
pop: true
- match: \.\.
scope: keyword.operator.range.fish
- include: command-substitution
- include: variable-expansion
- include: string-quoted
- match: '(?:[+-]?[0-9]+)(?=[\s;&)|<>]|\]|\.\.)'
scope: constant.numeric.fish
- match: '(?![\s''"]|\.\.)'
comment: 'Begin/end string as before with the addition of breaking at a '']'' or ".."'
push:
- match: '(?=[\s;&)|<>''"]|\]|\.\.)'
pop: true
- include: string-unquoted-patterns
line-continuation:
- match: (?=\\\n)
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
push:
- match: '(?=\n)|^(?![\s\#\\])|\s(?![\s\#])'
pop: true
- match: \\\n
scope: constant.character.escape
- match: '\#'
captures:
0: punctuation.definition.comment.fish
push:
- meta_scope: comment.line.continuation.fish
- match: \n
pop: true
parameter:
- match: '(?![\s;&)|<>^])'
comment: See the argument rule for more general information on parameters
push:
- match: '(?=[\s;&)|<>])'
pop: true
- match: '(?:--)(?=[\s;&)|<>])'
comment: End of options (parameter of just two hyphens)
scope: meta.parameter.option.end.fish variable.parameter.fish punctuation.definition.option.end.fish meta.string.unquoted.fish
- match: (?=--)
comment: Long option (parameter starting with two hyphens)
push:
- meta_scope: meta.parameter.option.long.fish
- match: '(?=[\s;&)|<>])'
pop: true
- match: (?:--)
captures:
0: punctuation.definition.option.long.begin.fish meta.string.unquoted.fish
push:
- meta_scope: variable.parameter.fish
- match: '(?=[\s;&)|<>]|=)'
pop: true
- include: command-substitution
- match: (?=\$)
push:
- meta_scope: meta.string.unquoted.fish
- match: (?!\$)
pop: true
- include: variable-expansion
- include: string-quoted
- match: '(?![''"])'
push:
- meta_scope: meta.string.unquoted.fish
- match: '(?=[\s;&()|<>''"$]|\=)'
pop: true
- include: string-unquoted-patterns
- match: (?:=)
comment: Consume the '=' and then use standard parameter patterns as well as numerics
captures:
0: variable.parameter.fish punctuation.definition.option.long.separator.fish meta.string.unquoted.fish
push:
- match: '(?=[\s;&)|<>])'
pop: true
- match: '(?:[+-]?)(?:[0-9]+\.?[0-9]*|[0-9]*\.?[0-9]+)(?=$|[\s;&)|<>])'
scope: meta.string.unquoted.fish constant.numeric.fish
- include: parameter-patterns
- match: '(?:-)(?=[^\s;&)|<>])'
comment: Short option (parameter starting with one hyphen)
captures:
0: punctuation.definition.option.short.fish meta.string.unquoted.fish
push:
- meta_scope: meta.parameter.option.short.fish variable.parameter.fish
- match: '(?=[\s;&)|<>])'
pop: true
- include: parameter-patterns
- include: argument
parameter-patterns:
- include: command-substitution
- match: (?=\$)
comment: Give variable expansion the unquoted string scope
push:
- meta_scope: meta.string.unquoted.fish
- match: (?!\$)
pop: true
- include: variable-expansion
- include: string
pipeline:
- match: '(?:[&|]|(?:[0-9]+)?(?:<|>>?|\^\^?))'
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)
scope: invalid.illegal.operator.fish
- match: (and|or)\b(?!\s+-)
comment: Todo - These commands cannot be followed by backgrounding, piping, or redirection alone. Add logic to catch these cases. It will be extensive...
scope: meta.function-call.fish keyword.operator.word.fish
- include: line-continuation
- match: (not)\b(?!\s+-)
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
scope: meta.function-call.fish keyword.operator.word.fish
- match: '(?:case|else|end)(?=[\s;&)|<>])'
comment: Match a command which is illegal in the base scope
scope: invalid.illegal.function-call.fish
- match: '(?=[^\s#])'
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
push:
- match: '(\s*)(?=[\n;&)])'
captures:
1: meta.function-call.fish
pop: true
- match: |-
(?x)
(?# Negative lookahead for piping)
(?!
(?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
)
comment: Match the first command of a potential pipeline
push:
- meta_scope: meta.function-call.fish
- match: |-
(?x)
(?# Look ahead for operators after whitespace)
(?=\s*
(?:
(?# Find a control operator)
[\n;&)]
|
(?# Find a pipe operator)
(?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
)
)
pop: true
- include: command-call-meta
- include: command-call-standard
- match: |-
(?x)
(?# Look ahead for piping)
(?=
(?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
)
comment: Match a second or later command of a pipeline, starting with the connective piping
push:
- meta_scope: meta.function-call.fish
- match: '(?=\s*[\n;&)])'
pop: true
- match: |-
(?x)
(?# Look ahead for piping followed by either control operators or piping)
(?=
(?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
\s*
(?:
$
|
[\n;&)]
|
(?:(?:[0-9]+)?(?:<|>>?|\^\^?))?\|
)
)
comment: Match a pipe not followed by a command, hence a malformed segment of the pipeline
push:
- match: '(?=\s*$|\s*[\n;&)])'
pop: true
- match: '(?:(?:[0-9]+)?(?:<|>|>>))?\|(?=\s*$|\s*[\n)])'
comment: If the pipeline would end implicitly (ie, with a newline or close parenthesis), then mark the pipe itself invalid
scope: invalid.illegal.operator.fish
- match: |-
(?x)
(?# Consume valid piping; captures 1 2 3)
(?:([0-9]+)?(<|>>?|\^\^?))?(\|)
(?# Consume whitespace)
\s*
(?# Consume remainder; capture 4)
(.*)
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
captures:
1: meta.pipe.fish constant.numeric.file-descriptor.fish
2: meta.pipe.fish keyword.operator.redirect.fish
3: meta.pipe.fish keyword.operator.pipe.fish
4: invalid.illegal.function-call.fish
- match: '(?:([0-9]+)?(<|>>?|\^\^?))?(\|)'
comment: Pick up a legitimate pipe
scope: meta.pipe.fish
captures:
1: constant.numeric.file-descriptor.fish
2: keyword.operator.pipe.redirect.fish
3: keyword.operator.pipe.fish
- include: line-continuation
- include: command-call-meta
- include: command-call-standard
redirection:
- match: '(?=(?:[0-9]+(?:<|>|>>)|>>|\^\^|[<>^])\&)'
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)
push:
- match: '(?=[\s;&)|]|(?:[0-9]+(?:<|>|>>)|>>|\^\^|[<>^])(?![&<>^]))'
pop: true
- match: '(?:([0-9]+)(<|>|>>)|(>>|\^\^|[<>^]))(\&)\s*'
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
captures:
1: constant.numeric.file-descriptor.fish
2: keyword.operator.redirect.fish
3: keyword.operator.redirect.fish
4: keyword.operator.redirect.dereference.fish
push:
- meta_scope: meta.redirection.fish
- match: '(\&.*$)|(?![&\\])'
captures:
1: invalid.illegal.file-descriptor.fish
pop: true
- include: line-continuation
- match: (?=\\\n)
push:
- meta_scope: meta.redirection.fish
- match: (?!\\\n)
pop: true
- include: line-continuation
- match: (?=\()
comment: Evaluates to a string which may be an integer
push:
- meta_scope: meta.redirection.fish
- match: (?!\()
pop: true
- include: command-substitution
- match: (?=\$)
comment: Evaluates to a string which may be an integer
push:
- meta_scope: meta.redirection.fish
- match: (?!\$)
pop: true
- include: variable-expansion
- match: '(?=[''"])'
comment: May be a quoted integer, which is allowed
push:
- meta_scope: meta.redirection.fish
- match: '(?![''"])'
pop: true
- include: string-quoted
- match: '(?:[0-9]+)(?=$|[\s;&)|<>])'
scope: meta.redirection.file-descriptor.fish constant.numeric.file-descriptor.fish
- match: '(?:-)(?=$|[\s;&)|<>])'
scope: meta.redirection.file-descriptor.fish keyword.operator.redirect.close.fish
- match: (?:\S+.*)$
comment: Anything else is illegal
scope: meta.redirection.fish invalid.illegal.file-descriptor.fish
- match: '(?=(?:[0-9]+(?:<|>|>>)|>>|\^\^|[<>^])\??)'
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
push:
- match: '(?=[\s;&)|]|(?:[0-9]+(?:<|>|>>)|>>|\^\^|[<>^])[&|])'
pop: true
- match: '(?:([0-9]+)(<|>|>>)|(>>|\^\^|[<>^]))(\?)?\s*'
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
captures:
1: constant.numeric.file-descriptor.fish
2: keyword.operator.redirect.fish
3: keyword.operator.redirect.fish
4: keyword.operator.redirect.clobber-test.fish
push:
- meta_scope: meta.redirection.fish
- match: "((?:[&?]|[0-9]*[<>^]).*$)|(?![&?<>^])"
captures:
1: invalid.illegal.path.fish
pop: true
- include: line-continuation
- match: (?=\\\n)
push:
- meta_scope: meta.redirection.fish
- match: (?!\\\n)
pop: true
- include: line-continuation
- match: (?=\()
comment: Evaluates to a string, so path cannot begin with '('
push:
- meta_scope: meta.redirection.fish
- match: (?!\()
pop: true
- include: command-substitution
- match: (?=\$)
comment: Evaluates to a string, so path cannot begin with '$'
push:
- meta_scope: meta.redirection.fish
- match: (?!\$)
pop: true
- include: variable-expansion
- match: "(?:[&?]|[0-9]*[<>^]).*$"
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
scope: meta.redirection.fish invalid.illegal.path.fish
- match: \~
scope: meta.redirection.path.fish keyword.operator.tilde.fish
- 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|[~%#])+)+'
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)
scope: meta.redirection.path.fish
string:
- include: string-quoted
- include: string-unquoted
string-quoted:
- match: \'
captures:
0: punctuation.definition.string.begin.fish
push:
- meta_scope: string.quoted.single.fish
- match: \'
captures:
0: punctuation.definition.string.end.fish
pop: true
- match: '\\[\''\\]'
comment: Only accepted escapes are \' and \\
scope: constant.character.escape.fish
- match: \"
captures:
0: punctuation.definition.string.begin.fish
push:
- meta_scope: string.quoted.double.fish
- match: \"
captures:
0: punctuation.definition.string.end.fish
pop: true
- match: '\\[\n\"\\$]'
comment: Only accepted escapes are \<newline>, \", \\, and \$
scope: constant.character.escape.fish
- include: variable-expansion
string-unquoted:
- match: '(?![\s;&()|<>''"$])'
comment: End unquoted string at anything that can't be in one
push:
- meta_scope: meta.string.unquoted.fish
- match: '(?=[\s;&()|<>''"$])'
pop: true
- include: string-unquoted-patterns
string-unquoted-patterns:
- match: |-
(?x)
\\[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[?-~]
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
scope: constant.character.escape.fish
- match: \\\n
comment: Just for convenience we separate the newline escape
scope: constant.character.escape.fish
- match: '\{'
captures:
0: punctuation.section.braces.begin.fish
push:
- meta_scope: meta.braces.brace-expansion.fish
- match: '(\})|(\n|[;&)|].*)'
captures:
1: punctuation.section.braces.end.fish
2: invalid.illegal.punctuation.section.fish
pop: true
- match: \,
scope: punctuation.section.braces.separator.fish
- include: command-substitution
- include: variable-expansion
- match: '(?:[^\S\n]+)'
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
scope: invalid.illegal.whitespace.fish
- include: string-quoted
- match: '(?:[+-]?)(?:[0-9]+\.?[0-9]*|[0-9]*\.?[0-9]+)(?=$|[\s;&)|<>]|\}|\,)'
scope: constant.numeric.fish
- match: '(?![\s;&)|<>''"])'
comment: "Begin/end string as before with the addition of breaking at a '}' or ','"
push:
- match: '(?=[\s;&)|<>''"]|\}|\,)'
pop: true
- match: \\\,
scope: constant.character.escape.fish
- include: string-unquoted-patterns
- match: (\*\*)|(\*)|(\?)
scope: meta.wildcard-expansion.fish
captures:
1: keyword.operator.double-star.fish
2: keyword.operator.single-star.fish
3: keyword.operator.question-mark.fish
variable-expansion:
- include: variable-expansion-illegal
- match: (?=\$)
comment: 'Capture "$foo" or "$foo[]" or "$$foo[][]" etc'
push:
- meta_scope: meta.variable-expansion.fish
- match: '(?=[^\$\w\[])'
pop: true
- match: \$
captures:
0: punctuation.definition.variable.fish
push:
- meta_scope: variable.other.fish
- match: '(?=[^\$\w])'
pop: true
- include: variable-expansion-illegal
- include: variable-expansion-simple
- include: index-expansion
variable-expansion-illegal:
- match: '\$(?:(?=[,''"\]}\s;&)|])|[^\w\$][^$,''"\]}\s;&)|]*)'
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
scope: invalid.illegal.variable-expansion.fish
variable-expansion-simple:
- match: \$
captures:
0: punctuation.definition.variable.fish
push:
- meta_scope: variable.other.fish
- match: '(?=[^\$\w])'
pop: true
- include: variable-expansion-illegal
- include: variable-expansion-simple