1165 lines
29 KiB
JavaScript
1165 lines
29 KiB
JavaScript
|
/**
|
||
|
* @file Bash grammar for tree-sitter
|
||
|
* @author Max Brunsfeld <maxbrunsfeld@gmail.com>
|
||
|
* @author Amaan Qureshi <amaanq12@gmail.com>
|
||
|
* @license MIT
|
||
|
*/
|
||
|
|
||
|
/* eslint-disable arrow-parens */
|
||
|
/* eslint-disable camelcase */
|
||
|
/* eslint-disable-next-line spaced-comment */
|
||
|
/// <reference types="tree-sitter-cli/dsl" />
|
||
|
// @ts-check
|
||
|
|
||
|
const SPECIAL_CHARACTERS = [
|
||
|
'\'', '"',
|
||
|
'<', '>',
|
||
|
'{', '}',
|
||
|
'\\[', '\\]',
|
||
|
'(', ')',
|
||
|
'`', '$',
|
||
|
'|', '&', ';',
|
||
|
'\\',
|
||
|
'\\s',
|
||
|
];
|
||
|
|
||
|
const PREC = {
|
||
|
UPDATE: 0,
|
||
|
ASSIGN: 1,
|
||
|
TERNARY: 2,
|
||
|
LOGICAL_OR: 3,
|
||
|
LOGICAL_AND: 4,
|
||
|
BITWISE_OR: 5,
|
||
|
BITWISE_XOR: 6,
|
||
|
BITWISE_AND: 7,
|
||
|
EQUALITY: 8,
|
||
|
COMPARE: 9,
|
||
|
TEST: 10,
|
||
|
UNARY: 11,
|
||
|
SHIFT: 12,
|
||
|
ADD: 13,
|
||
|
MULTIPLY: 14,
|
||
|
EXPONENT: 15,
|
||
|
NEGATE: 16,
|
||
|
PREFIX: 17,
|
||
|
POSTFIX: 18,
|
||
|
};
|
||
|
|
||
|
module.exports = grammar({
|
||
|
name: 'bash',
|
||
|
|
||
|
conflicts: $ => [
|
||
|
[$._expression, $.command_name],
|
||
|
[$.command, $.variable_assignments],
|
||
|
[$.redirected_statement, $.command],
|
||
|
[$.redirected_statement, $.command_substitution],
|
||
|
[$.function_definition, $.command_name],
|
||
|
[$.pipeline],
|
||
|
],
|
||
|
|
||
|
inline: $ => [
|
||
|
$._statement,
|
||
|
$._terminator,
|
||
|
$._literal,
|
||
|
$._terminated_statement,
|
||
|
$._primary_expression,
|
||
|
$._simple_variable_name,
|
||
|
$._multiline_variable_name,
|
||
|
$._special_variable_name,
|
||
|
$._c_word,
|
||
|
$._statement_not_subshell,
|
||
|
],
|
||
|
|
||
|
externals: $ => [
|
||
|
$.heredoc_start,
|
||
|
$.simple_heredoc_body,
|
||
|
$._heredoc_body_beginning,
|
||
|
$.heredoc_content,
|
||
|
$.heredoc_end,
|
||
|
$.file_descriptor,
|
||
|
$._empty_value,
|
||
|
$._concat,
|
||
|
$.variable_name, // Variable name followed by an operator like '=' or '+='
|
||
|
$.test_operator,
|
||
|
$.regex,
|
||
|
$._regex_no_slash,
|
||
|
$._regex_no_space,
|
||
|
$._expansion_word,
|
||
|
$.extglob_pattern,
|
||
|
$._bare_dollar,
|
||
|
$._brace_start,
|
||
|
$._immediate_double_hash,
|
||
|
$._external_expansion_sym_hash,
|
||
|
$._external_expansion_sym_bang,
|
||
|
$._external_expansion_sym_equal,
|
||
|
'}',
|
||
|
']',
|
||
|
'<<',
|
||
|
'<<-',
|
||
|
/\n/,
|
||
|
$.__error_recovery,
|
||
|
],
|
||
|
|
||
|
extras: $ => [
|
||
|
$.comment,
|
||
|
/\s/,
|
||
|
/\\\r?\n/,
|
||
|
/\\( |\t|\v|\f)/,
|
||
|
],
|
||
|
|
||
|
supertypes: $ => [
|
||
|
$._statement,
|
||
|
$._expression,
|
||
|
$._primary_expression,
|
||
|
],
|
||
|
|
||
|
word: $ => $.word,
|
||
|
|
||
|
rules: {
|
||
|
program: $ => optional($._statements),
|
||
|
|
||
|
_statements: $ => prec(1, seq(
|
||
|
repeat(seq(
|
||
|
$._statement,
|
||
|
$._terminator,
|
||
|
)),
|
||
|
$._statement,
|
||
|
optional($._terminator),
|
||
|
)),
|
||
|
|
||
|
_terminated_statement: $ => repeat1(seq(
|
||
|
$._statement,
|
||
|
$._terminator,
|
||
|
)),
|
||
|
|
||
|
// Statements
|
||
|
|
||
|
_statement: $ => choice(
|
||
|
$._statement_not_subshell,
|
||
|
$.subshell,
|
||
|
),
|
||
|
|
||
|
_statement_not_subshell: $ => choice(
|
||
|
$.redirected_statement,
|
||
|
$.variable_assignment,
|
||
|
$.variable_assignments,
|
||
|
$.command,
|
||
|
$.declaration_command,
|
||
|
$.unset_command,
|
||
|
$.test_command,
|
||
|
$.negated_command,
|
||
|
$.for_statement,
|
||
|
$.c_style_for_statement,
|
||
|
$.while_statement,
|
||
|
$.if_statement,
|
||
|
$.case_statement,
|
||
|
$.pipeline,
|
||
|
$.list,
|
||
|
$.compound_statement,
|
||
|
$.function_definition,
|
||
|
),
|
||
|
|
||
|
_statement_not_pipeline: $ => prec(1, choice(
|
||
|
$.redirected_statement,
|
||
|
$.variable_assignment,
|
||
|
$.variable_assignments,
|
||
|
$.command,
|
||
|
$.declaration_command,
|
||
|
$.unset_command,
|
||
|
$.test_command,
|
||
|
$.negated_command,
|
||
|
$.for_statement,
|
||
|
$.c_style_for_statement,
|
||
|
$.while_statement,
|
||
|
$.if_statement,
|
||
|
$.case_statement,
|
||
|
$.list,
|
||
|
$.compound_statement,
|
||
|
$.function_definition,
|
||
|
$.subshell,
|
||
|
)),
|
||
|
|
||
|
redirected_statement: $ => prec.dynamic(-1, prec(-1, choice(
|
||
|
seq(
|
||
|
field('body', $._statement),
|
||
|
field('redirect', choice(
|
||
|
repeat1(choice(
|
||
|
$.file_redirect,
|
||
|
$.heredoc_redirect,
|
||
|
)),
|
||
|
)),
|
||
|
),
|
||
|
seq(
|
||
|
field('body', choice($.if_statement, $.while_statement)),
|
||
|
$.herestring_redirect,
|
||
|
),
|
||
|
field('redirect', repeat1($.file_redirect)),
|
||
|
$.herestring_redirect,
|
||
|
))),
|
||
|
|
||
|
for_statement: $ => seq(
|
||
|
choice('for', 'select'),
|
||
|
field('variable', $._simple_variable_name),
|
||
|
optional(seq(
|
||
|
'in',
|
||
|
field('value', repeat1($._literal)),
|
||
|
)),
|
||
|
$._terminator,
|
||
|
field('body', $.do_group),
|
||
|
),
|
||
|
|
||
|
c_style_for_statement: $ => seq(
|
||
|
'for',
|
||
|
'((',
|
||
|
choice($._for_body),
|
||
|
'))',
|
||
|
optional(';'),
|
||
|
field('body', choice(
|
||
|
$.do_group,
|
||
|
$.compound_statement,
|
||
|
)),
|
||
|
),
|
||
|
_for_body: $ => seq(
|
||
|
field('initializer', commaSep($._c_expression)),
|
||
|
$._c_terminator,
|
||
|
field('condition', commaSep($._c_expression)),
|
||
|
$._c_terminator,
|
||
|
field('update', commaSep($._c_expression)),
|
||
|
),
|
||
|
|
||
|
_c_expression: $ => choice(
|
||
|
$._c_expression_not_assignment,
|
||
|
alias($._c_variable_assignment, $.variable_assignment),
|
||
|
),
|
||
|
_c_expression_not_assignment: $ => choice(
|
||
|
$._c_word,
|
||
|
$.simple_expansion,
|
||
|
$.expansion,
|
||
|
$.number,
|
||
|
$.string,
|
||
|
alias($._c_unary_expression, $.unary_expression),
|
||
|
alias($._c_binary_expression, $.binary_expression),
|
||
|
alias($._c_postfix_expression, $.postfix_expression),
|
||
|
alias($._c_parenthesized_expression, $.parenthesized_expression),
|
||
|
$.command_substitution,
|
||
|
),
|
||
|
|
||
|
_c_variable_assignment: $ => seq(
|
||
|
field('name', alias($._c_word, $.variable_name)),
|
||
|
'=',
|
||
|
field('value', $._c_expression),
|
||
|
),
|
||
|
_c_unary_expression: $ => prec(PREC.PREFIX, seq(
|
||
|
field('operator', choice('++', '--')),
|
||
|
$._c_expression_not_assignment,
|
||
|
)),
|
||
|
_c_binary_expression: $ => {
|
||
|
const table = [
|
||
|
[choice('+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '&=', '^=', '|='), PREC.UPDATE],
|
||
|
[choice('||', '-o'), PREC.LOGICAL_OR],
|
||
|
[choice('&&', '-a'), PREC.LOGICAL_AND],
|
||
|
['|', PREC.BITWISE_OR],
|
||
|
['^', PREC.BITWISE_XOR],
|
||
|
['&', PREC.BITWISE_AND],
|
||
|
[choice('==', '!='), PREC.EQUALITY],
|
||
|
[choice('<', '>', '<=', '>='), PREC.COMPARE],
|
||
|
[choice('<<', '>>'), PREC.SHIFT],
|
||
|
[choice('+', '-'), PREC.ADD],
|
||
|
[choice('*', '/', '%'), PREC.MULTIPLY],
|
||
|
['**', PREC.EXPONENT],
|
||
|
];
|
||
|
|
||
|
return choice(...table.map(([operator, precedence]) => {
|
||
|
// @ts-ignore
|
||
|
return prec[operator === '**' ? 'right' : 'left'](precedence, seq(
|
||
|
field('left', $._c_expression_not_assignment),
|
||
|
// @ts-ignore
|
||
|
field('operator', operator),
|
||
|
field('right', $._c_expression_not_assignment),
|
||
|
));
|
||
|
}));
|
||
|
},
|
||
|
_c_postfix_expression: $ => prec(PREC.POSTFIX, seq(
|
||
|
$._c_expression_not_assignment,
|
||
|
field('operator', choice('++', '--')),
|
||
|
)),
|
||
|
_c_parenthesized_expression: $ => seq(
|
||
|
'(',
|
||
|
commaSep1($._c_expression),
|
||
|
')',
|
||
|
),
|
||
|
_c_word: $ => alias(/[a-zA-Z_][a-zA-Z0-9_]*/, $.word),
|
||
|
|
||
|
while_statement: $ => seq(
|
||
|
choice('while', 'until'),
|
||
|
field('condition', $._terminated_statement),
|
||
|
field('body', $.do_group),
|
||
|
),
|
||
|
|
||
|
do_group: $ => seq(
|
||
|
'do',
|
||
|
optional($._terminated_statement),
|
||
|
'done',
|
||
|
),
|
||
|
|
||
|
if_statement: $ => seq(
|
||
|
'if',
|
||
|
field('condition', $._terminated_statement),
|
||
|
'then',
|
||
|
optional($._terminated_statement),
|
||
|
repeat($.elif_clause),
|
||
|
optional($.else_clause),
|
||
|
'fi',
|
||
|
),
|
||
|
|
||
|
elif_clause: $ => seq(
|
||
|
'elif',
|
||
|
$._terminated_statement,
|
||
|
'then',
|
||
|
optional($._terminated_statement),
|
||
|
),
|
||
|
|
||
|
else_clause: $ => seq(
|
||
|
'else',
|
||
|
optional($._terminated_statement),
|
||
|
),
|
||
|
|
||
|
case_statement: $ => seq(
|
||
|
'case',
|
||
|
field('value', $._literal),
|
||
|
optional($._terminator),
|
||
|
'in',
|
||
|
optional($._terminator),
|
||
|
optional(seq(
|
||
|
repeat($.case_item),
|
||
|
alias($.last_case_item, $.case_item),
|
||
|
)),
|
||
|
'esac',
|
||
|
),
|
||
|
|
||
|
case_item: $ => seq(
|
||
|
choice(
|
||
|
seq(
|
||
|
optional('('),
|
||
|
field('value', choice($._literal, $._extglob_blob)),
|
||
|
repeat(seq('|', field('value', choice($._literal, $._extglob_blob)))),
|
||
|
')',
|
||
|
),
|
||
|
),
|
||
|
optional($._statements),
|
||
|
prec(1, choice(
|
||
|
field('termination', ';;'),
|
||
|
field('fallthrough', choice(';&', ';;&')),
|
||
|
)),
|
||
|
),
|
||
|
|
||
|
last_case_item: $ => seq(
|
||
|
optional('('),
|
||
|
field('value', choice($._literal, $._extglob_blob)),
|
||
|
repeat(seq('|', field('value', choice($._literal, $._extglob_blob)))),
|
||
|
')',
|
||
|
optional($._statements),
|
||
|
optional(prec(1, ';;')),
|
||
|
),
|
||
|
|
||
|
function_definition: $ => prec.right(seq(
|
||
|
choice(
|
||
|
seq(
|
||
|
'function',
|
||
|
field('name', $.word),
|
||
|
optional(seq('(', ')')),
|
||
|
),
|
||
|
seq(
|
||
|
field('name', $.word),
|
||
|
'(', ')',
|
||
|
),
|
||
|
),
|
||
|
field(
|
||
|
'body',
|
||
|
choice(
|
||
|
$.compound_statement,
|
||
|
$.subshell,
|
||
|
$.test_command),
|
||
|
),
|
||
|
field('redirect', optional($.file_redirect)),
|
||
|
)),
|
||
|
|
||
|
compound_statement: $ => seq(
|
||
|
'{',
|
||
|
optional($._terminated_statement),
|
||
|
token(prec(-1, '}')),
|
||
|
),
|
||
|
|
||
|
subshell: $ => seq(
|
||
|
'(',
|
||
|
$._statements,
|
||
|
')',
|
||
|
),
|
||
|
|
||
|
pipeline: $ => prec.right(seq(
|
||
|
$._statement_not_pipeline,
|
||
|
repeat1(seq(
|
||
|
choice('|', '|&'),
|
||
|
$._statement_not_pipeline,
|
||
|
)),
|
||
|
)),
|
||
|
|
||
|
list: $ => prec.left(-1, seq(
|
||
|
$._statement,
|
||
|
choice('&&', '||'),
|
||
|
$._statement,
|
||
|
)),
|
||
|
|
||
|
// Commands
|
||
|
|
||
|
negated_command: $ => seq(
|
||
|
'!',
|
||
|
choice(
|
||
|
prec(2, $.command),
|
||
|
prec(1, $.variable_assignment),
|
||
|
$.test_command,
|
||
|
$.subshell,
|
||
|
),
|
||
|
),
|
||
|
|
||
|
test_command: $ => seq(
|
||
|
choice(
|
||
|
seq('[', optional(choice($._expression, $.redirected_statement)), ']'),
|
||
|
seq('[[', $._expression, ']]'),
|
||
|
seq('((', optional($._expression), '))'),
|
||
|
),
|
||
|
),
|
||
|
|
||
|
declaration_command: $ => prec.left(seq(
|
||
|
choice('declare', 'typeset', 'export', 'readonly', 'local'),
|
||
|
repeat(choice(
|
||
|
$._literal,
|
||
|
$._simple_variable_name,
|
||
|
$.variable_assignment,
|
||
|
)),
|
||
|
)),
|
||
|
|
||
|
unset_command: $ => prec.left(seq(
|
||
|
choice('unset', 'unsetenv'),
|
||
|
repeat(choice(
|
||
|
$._literal,
|
||
|
$._simple_variable_name,
|
||
|
)),
|
||
|
)),
|
||
|
|
||
|
command: $ => prec.left(seq(
|
||
|
repeat(choice(
|
||
|
$.variable_assignment,
|
||
|
field('redirect', choice($.file_redirect, $.herestring_redirect)),
|
||
|
)),
|
||
|
field('name', $.command_name),
|
||
|
choice(
|
||
|
repeat(choice(
|
||
|
field('argument', $._literal),
|
||
|
field('argument', alias($._bare_dollar, '$')),
|
||
|
field('argument', seq(
|
||
|
choice('=~', '=='),
|
||
|
choice($._literal, $.regex),
|
||
|
)),
|
||
|
field('redirect', $.herestring_redirect),
|
||
|
)),
|
||
|
$.subshell,
|
||
|
),
|
||
|
)),
|
||
|
|
||
|
command_name: $ => $._literal,
|
||
|
|
||
|
variable_assignment: $ => seq(
|
||
|
field('name', choice(
|
||
|
$.variable_name,
|
||
|
$.subscript,
|
||
|
)),
|
||
|
choice(
|
||
|
'=',
|
||
|
'+=',
|
||
|
),
|
||
|
field('value', choice(
|
||
|
$._literal,
|
||
|
$.array,
|
||
|
$._empty_value,
|
||
|
alias($._comment_word, $.word),
|
||
|
)),
|
||
|
),
|
||
|
|
||
|
variable_assignments: $ => seq($.variable_assignment, repeat1($.variable_assignment)),
|
||
|
|
||
|
subscript: $ => seq(
|
||
|
field('name', $.variable_name),
|
||
|
'[',
|
||
|
field('index', choice($._literal, $.binary_expression, $.unary_expression, $.parenthesized_expression)),
|
||
|
optional($._concat),
|
||
|
']',
|
||
|
optional($._concat),
|
||
|
),
|
||
|
|
||
|
file_redirect: $ => prec.left(seq(
|
||
|
field('descriptor', optional($.file_descriptor)),
|
||
|
choice(
|
||
|
seq(
|
||
|
choice('<', '>', '>>', '&>', '&>>', '<&', '>&', '>|'),
|
||
|
field('destination', repeat1($._literal)),
|
||
|
),
|
||
|
seq(
|
||
|
choice('<&-', '>&-'), // close file descriptor
|
||
|
optional(field('destination', $._literal)),
|
||
|
),
|
||
|
),
|
||
|
)),
|
||
|
|
||
|
heredoc_redirect: $ => seq(
|
||
|
field('descriptor', optional($.file_descriptor)),
|
||
|
choice('<<', '<<-'),
|
||
|
$.heredoc_start,
|
||
|
optional(choice(
|
||
|
alias($._heredoc_pipeline, $.pipeline),
|
||
|
seq(
|
||
|
field('redirect', repeat1($.file_redirect)),
|
||
|
optional($._heredoc_expression),
|
||
|
),
|
||
|
$._heredoc_expression,
|
||
|
$._heredoc_command,
|
||
|
)),
|
||
|
/\n/,
|
||
|
choice($._heredoc_body, $._simple_heredoc_body),
|
||
|
),
|
||
|
|
||
|
_heredoc_pipeline: $ => seq(
|
||
|
choice('|', '|&'),
|
||
|
$._statement,
|
||
|
),
|
||
|
|
||
|
_heredoc_expression: $ => seq(
|
||
|
field('operator', choice('||', '&&')),
|
||
|
field('right', $._statement),
|
||
|
),
|
||
|
|
||
|
_heredoc_command: $ => repeat1(field('argument', $._literal)),
|
||
|
|
||
|
_heredoc_body: $ => seq(
|
||
|
$.heredoc_body,
|
||
|
$.heredoc_end,
|
||
|
),
|
||
|
|
||
|
heredoc_body: $ => seq(
|
||
|
$._heredoc_body_beginning,
|
||
|
repeat(choice(
|
||
|
$.expansion,
|
||
|
$.simple_expansion,
|
||
|
$.command_substitution,
|
||
|
$.heredoc_content,
|
||
|
)),
|
||
|
),
|
||
|
|
||
|
_simple_heredoc_body: $ => seq(
|
||
|
alias($.simple_heredoc_body, $.heredoc_body),
|
||
|
$.heredoc_end,
|
||
|
),
|
||
|
|
||
|
herestring_redirect: $ => prec.left(seq(
|
||
|
field('descriptor', optional($.file_descriptor)),
|
||
|
'<<<',
|
||
|
$._literal,
|
||
|
)),
|
||
|
|
||
|
// Expressions
|
||
|
|
||
|
_expression: $ => choice(
|
||
|
$._literal,
|
||
|
$.unary_expression,
|
||
|
$.ternary_expression,
|
||
|
$.binary_expression,
|
||
|
$.postfix_expression,
|
||
|
$.parenthesized_expression,
|
||
|
),
|
||
|
|
||
|
// https://tldp.org/LDP/abs/html/opprecedence.html
|
||
|
binary_expression: $ => {
|
||
|
const table = [
|
||
|
[choice('+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '&=', '^=', '|='), PREC.UPDATE],
|
||
|
[choice('=', '=~'), PREC.ASSIGN],
|
||
|
['||', PREC.LOGICAL_OR],
|
||
|
['&&', PREC.LOGICAL_AND],
|
||
|
['|', PREC.BITWISE_OR],
|
||
|
['^', PREC.BITWISE_XOR],
|
||
|
['&', PREC.BITWISE_AND],
|
||
|
[choice('==', '!='), PREC.EQUALITY],
|
||
|
[choice('<', '>', '<=', '>='), PREC.COMPARE],
|
||
|
[$.test_operator, PREC.TEST],
|
||
|
[choice('<<', '>>'), PREC.SHIFT],
|
||
|
[choice('+', '-'), PREC.ADD],
|
||
|
[choice('*', '/', '%'), PREC.MULTIPLY],
|
||
|
['**', PREC.EXPONENT],
|
||
|
];
|
||
|
|
||
|
return choice(
|
||
|
choice(...table.map(([operator, precedence]) => {
|
||
|
// @ts-ignore
|
||
|
return prec[operator === '**' ? 'right' : 'left'](precedence, seq(
|
||
|
field('left', $._expression),
|
||
|
// @ts-ignore
|
||
|
field('operator', operator),
|
||
|
field('right', $._expression),
|
||
|
));
|
||
|
})),
|
||
|
prec(PREC.ASSIGN, seq(
|
||
|
field('left', $._expression),
|
||
|
field('operator', '=~'),
|
||
|
field('right', alias($._regex_no_space, $.regex)),
|
||
|
)),
|
||
|
prec(PREC.EQUALITY, seq(
|
||
|
field('left', $._expression),
|
||
|
field('operator', choice('==', '!=')),
|
||
|
field('right', $._extglob_blob),
|
||
|
)),
|
||
|
);
|
||
|
},
|
||
|
|
||
|
ternary_expression: $ => prec.left(PREC.TERNARY, seq(
|
||
|
field('condition', $._expression),
|
||
|
'?',
|
||
|
field('consequence', $._expression),
|
||
|
':',
|
||
|
field('alternative', $._expression),
|
||
|
)),
|
||
|
|
||
|
unary_expression: $ => choice(
|
||
|
prec(PREC.PREFIX, seq(
|
||
|
field('operator', tokenLiterals(1, '++', '--')),
|
||
|
$._expression,
|
||
|
)),
|
||
|
prec(PREC.UNARY, seq(
|
||
|
field('operator', tokenLiterals(1, '-', '+', '~')),
|
||
|
$._expression,
|
||
|
)),
|
||
|
prec.right(PREC.UNARY, seq(
|
||
|
field('operator', '!'),
|
||
|
$._expression,
|
||
|
)),
|
||
|
prec.right(PREC.TEST, seq(
|
||
|
field('operator', $.test_operator),
|
||
|
$._expression,
|
||
|
)),
|
||
|
),
|
||
|
|
||
|
postfix_expression: $ => prec(PREC.POSTFIX, seq(
|
||
|
$._expression,
|
||
|
field('operator', choice('++', '--')),
|
||
|
)),
|
||
|
|
||
|
parenthesized_expression: $ => seq(
|
||
|
'(',
|
||
|
$._expression,
|
||
|
')',
|
||
|
),
|
||
|
|
||
|
// Literals
|
||
|
|
||
|
_literal: $ => choice(
|
||
|
$.concatenation,
|
||
|
$._primary_expression,
|
||
|
alias(prec(-2, repeat1($._special_character)), $.word),
|
||
|
),
|
||
|
|
||
|
_primary_expression: $ => choice(
|
||
|
$.word,
|
||
|
alias($.test_operator, $.word),
|
||
|
$.string,
|
||
|
$.raw_string,
|
||
|
$.translated_string,
|
||
|
$.ansi_c_string,
|
||
|
$.number,
|
||
|
$.expansion,
|
||
|
$.simple_expansion,
|
||
|
$.command_substitution,
|
||
|
$.process_substitution,
|
||
|
$.arithmetic_expansion,
|
||
|
$.brace_expression,
|
||
|
),
|
||
|
|
||
|
arithmetic_expansion: $ => choice(
|
||
|
seq(choice('$((', '(('), commaSep1($._arithmetic_expression), '))'),
|
||
|
seq('$[', $._arithmetic_expression, ']'),
|
||
|
),
|
||
|
|
||
|
brace_expression: $ => seq(
|
||
|
alias($._brace_start, '{'),
|
||
|
alias(token.immediate(/\d+/), $.number),
|
||
|
token.immediate('..'),
|
||
|
alias(token.immediate(/\d+/), $.number),
|
||
|
token.immediate('}'),
|
||
|
),
|
||
|
|
||
|
_arithmetic_expression: $ => prec(1, choice(
|
||
|
$._arithmetic_literal,
|
||
|
alias($._arithmetic_unary_expression, $.unary_expression),
|
||
|
alias($._arithmetic_ternary_expression, $.ternary_expression),
|
||
|
alias($._arithmetic_binary_expression, $.binary_expression),
|
||
|
alias($._arithmetic_postfix_expression, $.postfix_expression),
|
||
|
alias($._arithmetic_parenthesized_expression, $.parenthesized_expression),
|
||
|
$.command_substitution,
|
||
|
)),
|
||
|
|
||
|
_arithmetic_literal: $ => prec(1, choice(
|
||
|
$.number,
|
||
|
$.subscript,
|
||
|
$.simple_expansion,
|
||
|
$.expansion,
|
||
|
$._simple_variable_name,
|
||
|
$.variable_name,
|
||
|
$.string,
|
||
|
)),
|
||
|
|
||
|
_arithmetic_binary_expression: $ => {
|
||
|
const table = [
|
||
|
[choice('+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '&=', '^=', '|='), PREC.UPDATE],
|
||
|
[choice('=', '=~'), PREC.ASSIGN],
|
||
|
['||', PREC.LOGICAL_OR],
|
||
|
['&&', PREC.LOGICAL_AND],
|
||
|
['|', PREC.BITWISE_OR],
|
||
|
['^', PREC.BITWISE_XOR],
|
||
|
['&', PREC.BITWISE_AND],
|
||
|
[choice('==', '!='), PREC.EQUALITY],
|
||
|
[choice('<', '>', '<=', '>='), PREC.COMPARE],
|
||
|
[choice('<<', '>>'), PREC.SHIFT],
|
||
|
[choice('+', '-'), PREC.ADD],
|
||
|
[choice('*', '/', '%'), PREC.MULTIPLY],
|
||
|
['**', PREC.EXPONENT],
|
||
|
];
|
||
|
|
||
|
return choice(...table.map(([operator, precedence]) => {
|
||
|
// @ts-ignore
|
||
|
return prec.left(precedence, seq(
|
||
|
field('left', $._arithmetic_expression),
|
||
|
// @ts-ignore
|
||
|
field('operator', operator),
|
||
|
field('right', $._arithmetic_expression),
|
||
|
));
|
||
|
}));
|
||
|
},
|
||
|
|
||
|
_arithmetic_ternary_expression: $ => prec.left(PREC.TERNARY, seq(
|
||
|
field('condition', $._arithmetic_expression),
|
||
|
'?',
|
||
|
field('consequence', $._arithmetic_expression),
|
||
|
':',
|
||
|
field('alternative', $._arithmetic_expression),
|
||
|
)),
|
||
|
|
||
|
_arithmetic_unary_expression: $ => choice(
|
||
|
prec(PREC.PREFIX, seq(
|
||
|
field('operator', tokenLiterals(1, '++', '--')),
|
||
|
$._arithmetic_expression,
|
||
|
)),
|
||
|
prec(PREC.UNARY, seq(
|
||
|
field('operator', tokenLiterals(1, '-', '+', '~')),
|
||
|
$._arithmetic_expression,
|
||
|
)),
|
||
|
prec.right(PREC.UNARY, seq(
|
||
|
field('operator', '!'),
|
||
|
$._arithmetic_expression,
|
||
|
)),
|
||
|
),
|
||
|
|
||
|
_arithmetic_postfix_expression: $ => prec(PREC.POSTFIX, seq(
|
||
|
$._arithmetic_expression,
|
||
|
field('operator', choice('++', '--')),
|
||
|
)),
|
||
|
|
||
|
_arithmetic_parenthesized_expression: $ => seq(
|
||
|
'(',
|
||
|
$._arithmetic_expression,
|
||
|
')',
|
||
|
),
|
||
|
|
||
|
|
||
|
concatenation: $ => prec(-1, seq(
|
||
|
choice(
|
||
|
$._primary_expression,
|
||
|
alias($._special_character, $.word),
|
||
|
),
|
||
|
repeat1(seq(
|
||
|
choice($._concat, alias(/`\s*`/, '``')),
|
||
|
choice(
|
||
|
$._primary_expression,
|
||
|
alias($._special_character, $.word),
|
||
|
alias($._comment_word, $.word),
|
||
|
alias($._bare_dollar, '$'),
|
||
|
),
|
||
|
)),
|
||
|
optional(seq($._concat, '$')),
|
||
|
)),
|
||
|
|
||
|
_special_character: _ => token(prec(-1, choice('{', '}', '[', ']'))),
|
||
|
|
||
|
string: $ => seq(
|
||
|
'"',
|
||
|
repeat(seq(
|
||
|
choice(
|
||
|
seq(optional('$'), $.string_content),
|
||
|
$.expansion,
|
||
|
$.simple_expansion,
|
||
|
$.command_substitution,
|
||
|
$.arithmetic_expansion,
|
||
|
),
|
||
|
optional($._concat),
|
||
|
)),
|
||
|
optional('$'),
|
||
|
'"',
|
||
|
),
|
||
|
|
||
|
string_content: _ => token(prec(-1, /([^"`$\\\r\n]|\\(.|\r?\n))+/)),
|
||
|
|
||
|
translated_string: $ => seq('$', $.string),
|
||
|
|
||
|
array: $ => seq(
|
||
|
'(',
|
||
|
repeat($._literal),
|
||
|
')',
|
||
|
),
|
||
|
|
||
|
raw_string: _ => /'[^']*'/,
|
||
|
|
||
|
ansi_c_string: _ => /\$'([^']|\\')*'/,
|
||
|
|
||
|
number: $ => choice(
|
||
|
/-?(0x)?[0-9]+(#[0-9A-Za-z@_]+)?/,
|
||
|
// the base can be an expansion
|
||
|
seq(/-?(0x)?[0-9]+#/, $.expansion),
|
||
|
),
|
||
|
|
||
|
simple_expansion: $ => seq(
|
||
|
'$',
|
||
|
choice(
|
||
|
$._simple_variable_name,
|
||
|
$._multiline_variable_name,
|
||
|
$._special_variable_name,
|
||
|
$.variable_name,
|
||
|
alias('!', $.special_variable_name),
|
||
|
alias('#', $.special_variable_name),
|
||
|
),
|
||
|
),
|
||
|
|
||
|
string_expansion: $ => seq('$', $.string),
|
||
|
|
||
|
expansion: $ => seq(
|
||
|
'${',
|
||
|
optional($._expansion_body),
|
||
|
'}',
|
||
|
),
|
||
|
_expansion_body: $ => choice(
|
||
|
// ${!##} ${!#}
|
||
|
repeat1(field(
|
||
|
'operator',
|
||
|
choice(
|
||
|
alias($._external_expansion_sym_hash, '#'),
|
||
|
alias($._external_expansion_sym_bang, '!'),
|
||
|
alias($._external_expansion_sym_equal, '='),
|
||
|
),
|
||
|
)),
|
||
|
seq(
|
||
|
optional(field('operator', token.immediate('!'))),
|
||
|
choice($.variable_name, $._simple_variable_name, $._special_variable_name, $.subscript),
|
||
|
choice(
|
||
|
$._expansion_expression,
|
||
|
$._expansion_regex,
|
||
|
$._expansion_regex_replacement,
|
||
|
$._expansion_regex_removal,
|
||
|
$._expansion_max_length,
|
||
|
$._expansion_operator,
|
||
|
),
|
||
|
),
|
||
|
seq(
|
||
|
field('operator', token.immediate('!')),
|
||
|
choice($._simple_variable_name, $.variable_name),
|
||
|
optional(field('operator', choice(
|
||
|
token.immediate('@'),
|
||
|
token.immediate('*'),
|
||
|
))),
|
||
|
),
|
||
|
seq(
|
||
|
optional(field('operator', immediateLiterals('#', '!', '='))),
|
||
|
choice(
|
||
|
$.subscript,
|
||
|
$._simple_variable_name,
|
||
|
$._special_variable_name,
|
||
|
$.command_substitution,
|
||
|
),
|
||
|
repeat(field(
|
||
|
'operator',
|
||
|
choice(
|
||
|
alias($._external_expansion_sym_hash, '#'),
|
||
|
alias($._external_expansion_sym_bang, '!'),
|
||
|
alias($._external_expansion_sym_equal, '='),
|
||
|
),
|
||
|
)),
|
||
|
),
|
||
|
),
|
||
|
|
||
|
_expansion_expression: $ => prec(1, seq(
|
||
|
field('operator', immediateLiterals('=', ':=', '-', ':-', '+', ':+', '?', ':?')),
|
||
|
optional(seq(
|
||
|
choice(
|
||
|
alias($._concatenation_in_expansion, $.concatenation),
|
||
|
$.command_substitution,
|
||
|
$.word,
|
||
|
$.expansion,
|
||
|
$.simple_expansion,
|
||
|
$.array,
|
||
|
$.string,
|
||
|
$.raw_string,
|
||
|
$.ansi_c_string,
|
||
|
alias($._expansion_word, $.word),
|
||
|
),
|
||
|
)),
|
||
|
)),
|
||
|
|
||
|
_expansion_regex: $ => seq(
|
||
|
field('operator', choice('#', alias($._immediate_double_hash, '##'), '%', '%%')),
|
||
|
optional(choice($.regex, alias(')', $.regex), $.string, $.raw_string, alias(/\s+/, $.regex))),
|
||
|
),
|
||
|
|
||
|
_expansion_regex_replacement: $ => seq(
|
||
|
field('operator', choice('/', '//', '/#', '/%')),
|
||
|
optional(choice(
|
||
|
alias($._regex_no_slash, $.regex),
|
||
|
$.string,
|
||
|
$.command_substitution,
|
||
|
seq($.string, alias($._regex_no_slash, $.regex)),
|
||
|
)),
|
||
|
// This can be elided
|
||
|
optional(seq(
|
||
|
field('operator', '/'),
|
||
|
optional(seq(
|
||
|
choice(
|
||
|
$._primary_expression,
|
||
|
alias(prec(-2, repeat1($._special_character)), $.word),
|
||
|
seq($.command_substitution, alias($._expansion_word, $.word)),
|
||
|
alias($._expansion_word, $.word),
|
||
|
alias($._concatenation_in_expansion, $.concatenation),
|
||
|
$.array,
|
||
|
),
|
||
|
field('operator', optional('/')),
|
||
|
)),
|
||
|
)),
|
||
|
),
|
||
|
|
||
|
_expansion_regex_removal: $ => seq(
|
||
|
field('operator', choice(',', ',,', '^', '^^')),
|
||
|
optional($.regex),
|
||
|
),
|
||
|
|
||
|
_expansion_max_length: $ => seq(
|
||
|
field('operator', ':'),
|
||
|
optional(choice(
|
||
|
$._simple_variable_name,
|
||
|
$.number,
|
||
|
$.arithmetic_expansion,
|
||
|
$.expansion,
|
||
|
$.parenthesized_expression,
|
||
|
$.command_substitution,
|
||
|
alias($._expansion_max_length_binary_expression, $.binary_expression),
|
||
|
/\n/,
|
||
|
)),
|
||
|
optional(seq(
|
||
|
field('operator', ':'),
|
||
|
optional(choice(
|
||
|
$._simple_variable_name,
|
||
|
$.number,
|
||
|
$.arithmetic_expansion,
|
||
|
$.expansion,
|
||
|
$.parenthesized_expression,
|
||
|
$.command_substitution,
|
||
|
alias($._expansion_max_length_binary_expression, $.binary_expression),
|
||
|
/\n/,
|
||
|
)),
|
||
|
)),
|
||
|
),
|
||
|
|
||
|
_expansion_max_length_expression: $ => choice(
|
||
|
$._simple_variable_name,
|
||
|
$.number,
|
||
|
$.expansion,
|
||
|
alias($._expansion_max_length_binary_expression, $.binary_expression),
|
||
|
),
|
||
|
_expansion_max_length_binary_expression: $ => {
|
||
|
const table = [
|
||
|
[choice('+', '-'), PREC.ADD],
|
||
|
[choice('*', '/', '%'), PREC.MULTIPLY],
|
||
|
];
|
||
|
|
||
|
return choice(...table.map(([operator, precedence]) => {
|
||
|
// @ts-ignore
|
||
|
return prec.left(precedence, seq(
|
||
|
$._expansion_max_length_expression,
|
||
|
// @ts-ignore
|
||
|
field('operator', operator),
|
||
|
$._expansion_max_length_expression,
|
||
|
));
|
||
|
}));
|
||
|
},
|
||
|
|
||
|
_expansion_operator: _ => seq(
|
||
|
field('operator', token.immediate('@')),
|
||
|
field('operator', immediateLiterals('U', 'u', 'L', 'Q', 'E', 'P', 'A', 'K', 'a', 'k')),
|
||
|
),
|
||
|
|
||
|
_concatenation_in_expansion: $ => prec(-2, seq(
|
||
|
choice(
|
||
|
$.word,
|
||
|
$.variable_name,
|
||
|
$.simple_expansion,
|
||
|
$.expansion,
|
||
|
$.string,
|
||
|
$.raw_string,
|
||
|
$.ansi_c_string,
|
||
|
$.command_substitution,
|
||
|
alias($._expansion_word, $.word),
|
||
|
$.array,
|
||
|
),
|
||
|
repeat1(seq(
|
||
|
choice($._concat, alias(/`\s*`/, '``')),
|
||
|
choice(
|
||
|
$.word,
|
||
|
$.variable_name,
|
||
|
$.simple_expansion,
|
||
|
$.expansion,
|
||
|
$.string,
|
||
|
$.raw_string,
|
||
|
$.ansi_c_string,
|
||
|
$.command_substitution,
|
||
|
alias($._expansion_word, $.word),
|
||
|
$.array,
|
||
|
),
|
||
|
)),
|
||
|
)),
|
||
|
|
||
|
command_substitution: $ => choice(
|
||
|
seq('$(', $._statements, ')'),
|
||
|
seq('$(', field('redirect', $.file_redirect), ')'),
|
||
|
prec(1, seq('`', $._statements, '`')),
|
||
|
seq('$`', $._statements, '`'),
|
||
|
),
|
||
|
|
||
|
process_substitution: $ => seq(
|
||
|
choice('<(', '>('),
|
||
|
$._statements,
|
||
|
')',
|
||
|
),
|
||
|
|
||
|
_extglob_blob: $ => choice(
|
||
|
$.extglob_pattern,
|
||
|
seq(
|
||
|
$.extglob_pattern,
|
||
|
choice($.string, $.expansion, $.command_substitution),
|
||
|
optional($.extglob_pattern),
|
||
|
),
|
||
|
),
|
||
|
|
||
|
comment: _ => token(prec(-10, /#.*/)),
|
||
|
|
||
|
_comment_word: _ => token(prec(-8, seq(
|
||
|
choice(
|
||
|
noneOf(...SPECIAL_CHARACTERS),
|
||
|
seq('\\', noneOf('\\s')),
|
||
|
),
|
||
|
repeat(choice(
|
||
|
noneOf(...SPECIAL_CHARACTERS),
|
||
|
seq('\\', noneOf('\\s')),
|
||
|
'\\ ',
|
||
|
)),
|
||
|
))),
|
||
|
|
||
|
_simple_variable_name: $ => alias(/\w+/, $.variable_name),
|
||
|
_multiline_variable_name: $ => alias(
|
||
|
token(prec(-1, /(\w|\\\r?\n)+/)),
|
||
|
$.variable_name,
|
||
|
),
|
||
|
|
||
|
_special_variable_name: $ => alias(choice('*', '@', '?', '!', '#', '-', '$', '0', '_'), $.special_variable_name),
|
||
|
|
||
|
word: _ => token(seq(
|
||
|
choice(
|
||
|
noneOf('#', ...SPECIAL_CHARACTERS),
|
||
|
seq('\\', noneOf('\\s')),
|
||
|
),
|
||
|
repeat(choice(
|
||
|
noneOf(...SPECIAL_CHARACTERS),
|
||
|
seq('\\', noneOf('\\s')),
|
||
|
'\\ ',
|
||
|
)),
|
||
|
)),
|
||
|
|
||
|
_c_terminator: _ => choice(';', /\n/, '&'),
|
||
|
_terminator: _ => choice(';', ';;', /\n/, '&'),
|
||
|
},
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* Returns a regular expression that matches any character except the ones
|
||
|
* provided.
|
||
|
*
|
||
|
* @param {...string} characters
|
||
|
*
|
||
|
* @return {RegExp}
|
||
|
*
|
||
|
*/
|
||
|
function noneOf(...characters) {
|
||
|
const negatedString = characters.map(c => c == '\\' ? '\\\\' : c).join('');
|
||
|
return new RegExp('[^' + negatedString + ']');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a rule to optionally match one or more of the rules separated by a comma
|
||
|
*
|
||
|
* @param {RuleOrLiteral} rule
|
||
|
*
|
||
|
* @return {ChoiceRule}
|
||
|
*
|
||
|
*/
|
||
|
function commaSep(rule) {
|
||
|
return optional(commaSep1(rule));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a rule to match one or more of the rules separated by a comma
|
||
|
*
|
||
|
* @param {RuleOrLiteral} rule
|
||
|
*
|
||
|
* @return {SeqRule}
|
||
|
*
|
||
|
*/
|
||
|
function commaSep1(rule) {
|
||
|
return seq(rule, repeat(seq(',', rule)));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* Turns a list of rules into a choice of immediate rule
|
||
|
*
|
||
|
* @param {(RegExp|String)[]} literals
|
||
|
*
|
||
|
* @return {ChoiceRule}
|
||
|
*/
|
||
|
function immediateLiterals(...literals) {
|
||
|
return choice(...literals.map(l => token.immediate(l)));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* Turns a list of rules into a choice of aliased token rules
|
||
|
*
|
||
|
* @param {number} precedence
|
||
|
*
|
||
|
* @param {(RegExp|String)[]} literals
|
||
|
*
|
||
|
* @return {ChoiceRule}
|
||
|
*/
|
||
|
function tokenLiterals(precedence, ...literals) {
|
||
|
return choice(...literals.map(l => token(prec(precedence, l))));
|
||
|
}
|