Files
superwork/vscode/src/codeReader/languages/bash.ts
T

647 lines
28 KiB
TypeScript

import type { BlockShape, CodeBlock, CommandFacts, CommandWord, DeclaratorFacts, FunctionFacts, InterpreterFacts, LanguageAdapter, LocalSymbol, Node, OptionsFacts, PositionalParameter, Redirect, SourceRange, StepBlocks, SwitchFacts, TextKind, TokenRole } from '../contract'
import type { CommentRules, ListBlocks } from './shared'
import * as path from 'node:path'
import { bashNameReader } from './bashNames'
import { children, compoundShape, leafBlock, leafShape, listBlocks, named } from './shared'
/** 按习惯当常量用的名字:全大写(可带数字、下划线)。 */
const CONSTANT_NAME = /^[A-Z][A-Z0-9_]*$/
const POSITIONAL = /^[1-9]$/
/** 流程关键字(按关键字着色)和声明关键字(按声明着色)。 */
const FLOW_KEYWORDS = new Set(['if', 'then', 'else', 'elif', 'fi', 'for', 'while', 'until', 'do', 'done', 'case', 'esac', 'in', 'select'])
const DECLARATION_KEYWORDS = new Set(['local', 'export', 'readonly', 'declare', 'typeset', 'function', 'unset'])
/** 写在 `set` 里的单字母开关 → bash 的长名字(`set -o` 用的那个)。 */
const SHORT_SWITCHES: Record<string, string> = {
a: 'allexport',
b: 'notify',
e: 'errexit',
f: 'noglob',
h: 'hashall',
k: 'keyword',
m: 'monitor',
n: 'noexec',
t: 'onecmd',
u: 'nounset',
v: 'verbose',
x: 'xtrace',
B: 'braceexpand',
C: 'noclobber',
E: 'errtrace',
H: 'histexpand',
P: 'physical',
T: 'functrace',
}
/** 打印到输出的内置命令:函数里没重定向的这几句是函数的输出。 */
const PRINTERS = new Set(['echo', 'printf'])
const LOOPS = new Set(['for_statement', 'c_style_for_statement', 'while_statement'])
/** 分情况里一个情况最多几句、每句一行写完,才整个排成一张表(不拆成子块)。 */
const COMPACT_CASE_STATEMENTS = 3
const SIMPLE_STATEMENTS = new Set(['command', 'redirected_statement', 'variable_assignment', 'declaration_command', 'negated_command'])
/** 不出值的展开:命令替换、变量展开、算术展开、进程替换;值里有它们就不是写死的。 */
const EXPANSIONS = new Set(['simple_expansion', 'expansion', 'command_substitution', 'arithmetic_expansion', 'process_substitution'])
const COMMENTS: CommentRules = {
lineStyle: () => '#',
// 紧贴在函数上面一行的注释是函数的说明
isDoc: (comment) => {
const next = comment.nextSibling
return next?.type === 'function_definition' && next.startPosition.row === comment.endPosition.row + 1
},
documents: new Set(['function']),
}
function statements(items: Node[], afterRow: number, top: boolean): CodeBlock[] {
return listBlocks(items, afterRow, item => statementBlocks(item, top), COMMENTS)
}
const nested: ListBlocks = (items, afterRow) => statements(items, afterRow, false)
/** 节点里(不进嵌套的函数定义)满足 test 的节点,先序。 */
function descendants(node: Node, test: (node: Node) => boolean, out: Node[] = []): Node[] {
for (const child of children(node)) {
if (test(child))
out.push(child)
if (child.type !== 'function_definition')
descendants(child, test, out)
}
return out
}
function commandName(command: Node | null | undefined): string | undefined {
return command?.type === 'command' ? command.childForFieldName('name')?.text : undefined
}
/** 语句本身执行的那条命令:`cmd`,或带重定向的 `cmd >&2` 里的 cmd。 */
function commandOf(statement: Node): Node | undefined {
if (statement.type === 'command')
return statement
const body = statement.type === 'redirected_statement' ? statement.childForFieldName('body') : null
return body?.type === 'command' ? body : undefined
}
// ─── 块 ──────────────────────────────────────────────────────────────────────
function statementBlocks(node: Node, top: boolean): CodeBlock[] {
switch (node.type) {
case 'variable_assignment':
return [variableBlock(node, [node], top, false)]
case 'variable_assignments':
return [variableBlock(node, named(node).filter(child => child.type === 'variable_assignment'), top, false)]
case 'declaration_command':
return [declarationBlock(node, top)]
case 'function_definition':
return [functionBlock(node)]
case 'if_statement':
return ifBlocks(node)
case 'for_statement':
case 'c_style_for_statement':
case 'while_statement':
return [loopBlock('loop', node, node)]
case 'case_statement':
return [caseBlock(node)]
case 'pipeline':
return [pipelineBlock(node)]
case 'redirected_statement': {
const body = node.childForFieldName('body')
return body && body.type !== 'command' ? statementBlocks(body, top) : [commandBlock(node)]
}
case 'command':
return [commandBlock(node)]
case 'negated_command':
case 'test_command':
return [leafBlock(isTest(node) ? 'if' : 'command', node)]
case 'list':
return [leafBlock(conditionList(node) ? 'if' : 'command', node)]
default:
return [leafBlock('statement', node)]
}
}
/** 值里没有展开(变量、命令替换……),是写死的。 */
function isLiteral(value: Node): boolean {
return !EXPANSIONS.has(value.type) && named(value).every(isLiteral)
}
/** 常量:readonly 的,或脚本顶层、名字全大写、值写死的。 */
function variableBlock(node: Node, assignments: Node[], top: boolean, readonly: boolean): CodeBlock {
const declarators = assignments.flatMap((assignment): DeclaratorFacts[] => {
if (assignment.type !== 'variable_assignment')
return [{ name: assignment }]
const name = assignment.childForFieldName('name')
const value = assignment.childForFieldName('value')
return name ? [{ name, ...(value && { value }) }] : []
})
const constant = readonly || (top && declarators.length > 0 && declarators.every(({ name, value }) => CONSTANT_NAME.test(name.text) && value && isLiteral(value)))
return { kind: constant ? 'const' : 'variable', ...leafShape(node), facts: { declarators } }
}
/** local / export / readonly / declare:声明的变量是一块;什么都没声明(`export -f fn`)的当命令。 */
function declarationBlock(node: Node, top: boolean): CodeBlock {
const keyword = node.child(0)?.type
const declared = named(node).filter(child => child.type === 'variable_assignment' || child.type === 'variable_name')
if (declared.length === 0)
return leafBlock('command', node)
const readonly = keyword === 'readonly' || named(node).some(child => child.type === 'word' && /^-\w*r/.test(child.text))
return variableBlock(node, declared, top, readonly)
}
function commandBlock(node: Node): CodeBlock {
const command = commandOf(node)
const name = commandName(command)
if (name === 'set' && command && node.type === 'command') {
const switches = optionSwitches(named(command).filter(child => child.type !== 'command_name'))
if (switches)
return { kind: 'options', ...leafShape(node), facts: { switches } }
}
return leafBlock(name === 'return' ? 'return' : 'command', node)
}
/** `set -euo pipefail`、`set +x`、`set -o nounset` 的各个开关;有不是开关的词(`set -- a b`)时不是。 */
function optionSwitches(words: Node[]): OptionsFacts['switches'] | undefined {
const switches: SwitchFacts[] = []
for (let i = 0; i < words.length; i++) {
const text = words[i].text
const match = /^([-+])([a-z]+)$/i.exec(text)
if (!match)
return undefined
const on = match[1] === '-'
for (const letter of match[2]) {
if (letter === 'o') {
const name = words[++i]?.text
if (!name || !/^[a-z]+$/.test(name))
return undefined
switches.push({ id: name, on, flag: `${match[1]}o ${name}` })
continue
}
switches.push({ id: SHORT_SWITCHES[letter] ?? letter, on, flag: `${match[1]}${letter}` })
}
}
return switches.length > 0 ? switches : undefined
}
/** 条件:`[ … ]`、`[[ … ]]`,前面可以带 `!`。 */
function isTest(node: Node): boolean {
if (node.type === 'negated_command')
return named(node).some(isTest)
return node.type === 'test_command'
}
/** `a && b || c` 里用 && / || 连起来的各句,照源码顺序。 */
function listOperands(node: Node): Node[] {
return node.type === 'list' ? named(node).filter(child => child.type !== 'comment').flatMap(listOperands) : [node]
}
/**
* 当条件读的一串:全是条件(`[ -f x ] && [ … ]`,读成「成立:…」),或前面全是条件、最后跟一句(`[ … ] || continue`、
* `[ -f x ] && rm x`,读成「如果(不是)〔…〕,…」)。
*/
function conditionList(node: Node): boolean {
const operands = listOperands(node)
return operands.slice(0, -1).every(isTest) && (isTest(operands[operands.length - 1]) || operands.length > 1)
}
/** 语句块(`{ … }`、do … done、then 后面)里的各句连同注释:去掉关键字、分号。 */
function bodyItems(nodes: Node[]): Node[] {
return nodes.filter(child => child.isNamed)
}
/** 复合语句块:头部到第一句之前(没有句子时到 fromEnd 为止),子块是各句。 */
function compound(kind: TextKind, outer: Node, items: Node[], fromEnd: number): CodeBlock {
const shape: BlockShape = compoundShape(outer, items[0]?.startIndex ?? fromEnd, items, nested)
return { kind, ...shape }
}
/** 循环:头部到 do 为止,子块是 do … done 里的各句。管道后面跟的 while 也是这样拆,只是块本身是整个管道。 */
function loopBlock(kind: TextKind, outer: Node, loop: Node): CodeBlock {
const body = loop.childForFieldName('body')
const items = body ? bodyItems(children(body)) : []
const open = body?.child(0)
return compound(kind, outer, items, open?.endIndex ?? loop.endIndex)
}
/** if 一块,elif / else 展平成紧跟它的兄弟块(同 Python)。 */
function ifBlocks(node: Node): CodeBlock[] {
const clauses = named(node).filter(child => child.type === 'elif_clause' || child.type === 'else_clause')
return [clauseBlock('if', node), ...clauses.map(clause => clauseBlock('else', clause))]
}
/** if、elif 的主体是 then 后面、下一个 elif / else 之前的各句;else 的主体是 else 后面的各句。 */
function clauseBlock(kind: TextKind, node: Node): CodeBlock {
const all = children(node)
const opener = all.findIndex(child => child.type === (node.type === 'else_clause' ? 'else' : 'then'))
const rest = all.slice(opener + 1)
const end = rest.findIndex(child => child.type === 'elif_clause' || child.type === 'else_clause' || child.type === 'fi')
const items = bodyItems(end < 0 ? rest : rest.slice(0, end))
return compound(kind, node, items, all[opener]?.endIndex ?? node.endIndex)
}
function caseItems(node: Node): Node[] {
return named(node).filter(child => child.type === 'case_item')
}
/** 一个情况里要做的各句(连同注释):`)` 后面的、`;;` 之前的。 */
function caseBody(item: Node): Node[] {
const all = children(item)
const close = all.findIndex(child => child.type === ')')
return bodyItems(all.slice(close + 1))
}
/** 每个情况都只有几句、每句一行写完、没有注释和嵌套的语句块。 */
function compactCase(node: Node): boolean {
return caseItems(node).every((item) => {
const body = caseBody(item)
return body.length <= COMPACT_CASE_STATEMENTS && body.every(statement => SIMPLE_STATEMENTS.has(statement.type)
&& statement.startPosition.row === statement.endPosition.row
&& (statement.type !== 'redirected_statement' || commandOf(statement) !== undefined))
})
}
/** 分情况:几个情况都短时整条语句一块(情况排成表),否则头部到 in 为止、每个情况一块子块。 */
function caseBlock(node: Node): CodeBlock {
if (compactCase(node))
return leafBlock('case', node)
const items = caseItems(node)
const inKeyword = children(node).find(child => child.type === 'in')
const shape = compoundShape(node, items[0]?.startIndex ?? inKeyword?.endIndex ?? node.endIndex, [], nested)
return { kind: 'case', ...shape, children: [...shape.children, ...items.map(branchBlock)] }
}
/** 一个情况:头部是它的模式(到 `)` 为止),子块是要做的各句。 */
function branchBlock(item: Node): CodeBlock {
const body = caseBody(item)
const close = children(item).find(child => child.type === ')')
return compound('case', item, body, close?.endIndex ?? item.endIndex)
}
/** 管道:最后一段是循环(`find … | while read -r x; do … done`)时循环的主体是子块。 */
function pipelineBlock(node: Node): CodeBlock {
const last = named(node).at(-1)
if (last && LOOPS.has(last.type))
return loopBlock('pipeline', node, last)
return leafBlock('pipeline', node)
}
// ─── 函数 ────────────────────────────────────────────────────────────────────
/** `$1`、`"$1"`、`${1}` 这种只是取第几个参数的值:取的是第几个。 */
function positionalValue(value: Node | null): number | undefined {
if (!value)
return undefined
const inner = value.type === 'string' && named(value).length === 1 ? named(value)[0] : value
if (inner.type !== 'simple_expansion' && inner.type !== 'expansion')
return undefined
const expanded = named(inner)
const name = expanded.length === 1 ? expanded[0] : undefined
return name?.type === 'variable_name' && POSITIONAL.test(name.text) ? Number(name.text) : undefined
}
/** 函数里用到的第几个参数,和它们在函数里的名字(`local url="$1"`、`url=$1`)。 */
function positionalParameters(body: Node): PositionalParameter[] {
const used = new Set(descendants(body, node => node.type === 'variable_name' && POSITIONAL.test(node.text)
&& (node.parent?.type === 'simple_expansion' || node.parent?.type === 'expansion')).map(node => Number(node.text)))
const names = new Map<number, Node>()
for (const assignment of descendants(body, node => node.type === 'variable_assignment')) {
const position = positionalValue(assignment.childForFieldName('value'))
const name = assignment.childForFieldName('name')
if (position !== undefined && name && !names.has(position))
names.set(position, name)
}
return [...used].sort((a, b) => a - b).map(position => ({ position, ...(names.has(position) && { name: names.get(position) }) }))
}
/** 重定向了输出(`>&2`、`> file`)的语句不算打印到输出。 */
function printsToOutput(statement: Node): boolean {
const name = commandName(commandOf(statement))
if (!name || !PRINTERS.has(name))
return false
return redirectsOf(statement).every(redirect => redirect.kind === 'read' || redirect.stream === 'error')
}
/** 函数自己的各句(进 if、循环、分情况,不进命令替换、管道、嵌套的函数)里打印到输出的那几句。 */
function printedStatements(body: Node): Node[] {
const out: Node[] = []
const visit = (node: Node): void => {
for (const child of named(node)) {
if (child.type === 'command' || child.type === 'redirected_statement') {
if (printsToOutput(child))
out.push(child)
}
else if (child.type !== 'function_definition' && child.type !== 'pipeline' && child.type !== 'command_substitution') {
visit(child)
}
}
}
visit(body)
return out
}
/** 同一文件里有没有 `$(name …)`:函数的输出被拿去用了。 */
function captured(root: Node, name: string): boolean {
return descendants(root, node => node.type === 'command_substitution'
&& named(node).some(inner => commandName(inner) === name || commandName(listOperands(inner)[0]) === name)).length > 0
}
function functionBlock(node: Node): CodeBlock {
const name = node.childForFieldName('name')
const body = node.childForFieldName('body')
const items = body?.type === 'compound_statement' ? bodyItems(children(body)) : []
const open = body?.child(0)
const shape = compoundShape(node, items[0]?.startIndex ?? open?.endIndex ?? node.endIndex, items, nested)
const facts: FunctionFacts = {
...(name && { name }),
decorators: [],
parameters: [],
positional: body ? positionalParameters(body) : [],
prints: body ? printedStatements(body) : [],
captured: !!name && captured(node.tree.rootNode, name.text),
returns: body ? descendants(body, child => commandName(child) === 'return') : [],
}
return { kind: 'function', ...shape, facts }
}
// ─── 解释器 ──────────────────────────────────────────────────────────────────
/** `#!/usr/bin/env bash`、`#!/bin/sh -e`。 */
function interpreterFacts(shebang: Node): InterpreterFacts {
const command = shebang.text.slice(2).trim()
const [program = '', ...rest] = command.split(/\s+/)
if (path.posix.basename(program) === 'env') {
const named = rest.find(word => !word.startsWith('-'))
return { program: named ? path.posix.basename(named) : 'env', lookup: 'env', command }
}
return { program: path.posix.basename(program), lookup: 'path', command }
}
function programBlocks(root: Node): CodeBlock[] {
const items = named(root)
const [first] = items
if (first?.type === 'comment' && first.startPosition.row === 0 && first.text.startsWith('#!')) {
const interpreter: CodeBlock = { kind: 'interpreter', ...leafShape(first), facts: interpreterFacts(first) }
return [interpreter, ...statements(items.slice(1), 0, true)]
}
return statements(items, -1, true)
}
// ─── 命令 ────────────────────────────────────────────────────────────────────
/** 一个 `>`、`>>`、`>&2`、`2>&1`、`&>`、`<` 重定向;heredoc 这类不算。 */
function redirectOf(node: Node): Redirect | undefined {
if (node.type !== 'file_redirect')
return undefined
const descriptor = named(node).find(child => child.type === 'file_descriptor')?.text
const operator = children(node).find(child => !child.isNamed)?.type
const target = node.childForFieldName('destination')
if (!operator || !target)
return undefined
const stream = descriptor === '2' ? 'error' : 'output'
switch (operator) {
case '>&':
if (target.text === '1' || target.text === '2')
return { kind: 'merge', stream, into: target.text === '2' ? 'error' : 'output' }
return { kind: 'write', stream, target }
case '>':
case '>|':
return { kind: 'write', stream, target }
case '>>':
return { kind: 'append', stream, target }
case '&>':
return { kind: 'write', stream: 'both', target }
case '&>>':
return { kind: 'append', stream: 'both', target }
case '<':
return { kind: 'read', source: target }
default:
return undefined
}
}
function redirectsOf(statement: Node): Redirect[] {
if (statement.type !== 'redirected_statement')
return []
return statement.childrenForFieldName('redirect').flatMap((redirect) => {
const found = redirect ? redirectOf(redirect) : undefined
return found ? [found] : []
})
}
/** 一个词写死的文字:去掉引号、反斜杠转义;里面有展开的没有。 */
function literalOf(word: Node): string | undefined {
switch (word.type) {
case 'word':
case 'number':
case 'extglob_pattern':
return word.text.replace(/\\(.)/g, '$1')
case 'raw_string':
return word.text.slice(1, -1)
case 'command_name': {
const [inner] = named(word)
return inner ? literalOf(inner) : undefined
}
case 'string': {
const parts = named(word)
return parts.every(part => part.type === 'string_content') ? parts.map(part => part.text.replace(/\\([$`"\\])/g, '$1')).join('') : undefined
}
case 'concatenation': {
const parts = named(word).map(literalOf)
return parts.every(part => part !== undefined) ? parts.join('') : undefined
}
default:
return undefined
}
}
function commandWord(node: Node): CommandWord {
const literal = literalOf(node)
return literal === undefined ? { node } : { node, literal }
}
/** 一条命令(带不带重定向):命令名、后面的各个词(连写死的文字)、重定向。 */
function bashCommand(node: Node): CommandFacts | undefined {
const command = commandOf(node)
const name = command?.childForFieldName('name')
if (!command || !name)
return undefined
const args = command.childrenForFieldName('argument').filter(arg => arg !== null)
const inherited = node.type === 'command' ? redirectedChain(node) : undefined
return { name: commandWord(name), arguments: args.map(commandWord), redirects: [...redirectsOf(node), ...(inherited ? redirectsOf(inherited) : [])] }
}
/** 一串 `a && b` 里的最后一句。 */
function lastOperand(list: Node): Node | undefined {
return named(list).filter(child => child.type !== 'comment').at(-1)
}
/**
* tree-sitter-bash 把 `a && b > f` 读成整串带着重定向(redirected_statement 的 body 是 `a && b`),其实重定向只属于最后那条命令 b:
* command 是这样一串的最后一条时给出那个 redirected_statement,它的重定向算在 command 上。
*/
function redirectedChain(command: Node): Node | undefined {
let child = command
let parent = command.parent
while (parent?.type === 'list' && lastOperand(parent)?.id === child.id) {
child = parent
parent = parent.parent
}
return child.id !== command.id && parent?.type === 'redirected_statement' && parent.childForFieldName('body')?.id === child.id ? parent : undefined
}
// ─── 名字 ────────────────────────────────────────────────────────────────────
function rangeOf(node: Node): SourceRange {
return { startIndex: node.startIndex, endIndex: node.endIndex }
}
/**
* 同一文件里认得出的名字:函数(定义处是函数名,用到的是同名的命令),变量(定义处是赋值、local / export 声明、for 的变量、
* read 读进的名字,用到的是 `$x`、`${x…}` 和这些定义处)。第几个参数、`$?` 这类不算。
*/
function bashSymbols(root: Node): LocalSymbol[] {
const symbols = new Map<string, LocalSymbol>()
const symbol = (key: string): LocalSymbol => {
let found = symbols.get(key)
if (!found) {
found = { definitions: [], occurrences: [] }
symbols.set(key, found)
}
return found
}
const define = (key: string, node: Node): void => {
const entry = symbol(key)
entry.definitions.push(rangeOf(node))
entry.occurrences.push(rangeOf(node))
}
const functions = new Set(descendants(root, node => node.type === 'function_definition').map(fn => fn.childForFieldName('name')?.text))
const visit = (node: Node): void => {
const parent = node.parent
if (node.type === 'function_definition') {
const name = node.childForFieldName('name')
if (name)
define(`function:${name.text}`, name)
}
else if (node.type === 'word' && parent?.type === 'command_name' && functions.has(node.text)) {
symbol(`function:${node.text}`).occurrences.push(rangeOf(node))
}
else if (node.type === 'variable_name' && !POSITIONAL.test(node.text)) {
const defining = parent?.type === 'variable_assignment' || parent?.type === 'declaration_command' || parent?.type === 'for_statement'
if (defining)
define(`variable:${node.text}`, node)
else if (parent?.type === 'simple_expansion' || parent?.type === 'expansion')
symbol(`variable:${node.text}`).occurrences.push(rangeOf(node))
}
else if (node.type === 'command' && commandName(node) === 'read') {
for (const arg of node.childrenForFieldName('argument')) {
if (arg?.type === 'word' && /^[a-z_]\w*$/i.test(arg.text))
define(`variable:${arg.text}`, arg)
}
}
for (const child of children(node))
visit(child)
}
visit(root)
return [...symbols.values()].filter(entry => entry.definitions.length > 0)
}
// ─── 词性 ────────────────────────────────────────────────────────────────────
function insideString(leaf: Node): boolean {
for (let node = leaf.parent; node; node = node.parent) {
if (EXPANSIONS.has(node.type))
return false
if (node.type === 'string' || node.type === 'heredoc_body')
return true
}
return false
}
function bashTokenRole(leaf: Node): TokenRole {
const parent = leaf.parent
switch (leaf.type) {
case 'comment':
return 'comment'
case 'string_content':
case 'raw_string':
case 'ansi_c_string':
case 'heredoc_content':
case 'heredoc_body':
case 'extglob_pattern':
case 'regex':
return 'string'
case 'number':
return 'number'
case 'variable_name':
return CONSTANT_NAME.test(leaf.text) ? 'constant' : 'variable'
case 'special_variable_name':
return 'variable'
case 'test_operator':
return 'operator'
case 'file_descriptor':
return 'number'
case 'word':
if (parent?.type === 'command_name' || parent?.type === 'function_definition')
return 'function'
if (insideString(leaf) || parent?.type === 'case_item')
return 'string'
return /^\d+(?:\.\d+)*$/.test(leaf.text) ? 'number' : 'plain'
case '"':
return 'string'
}
if (FLOW_KEYWORDS.has(leaf.type))
return 'keyword'
return DECLARATION_KEYWORDS.has(leaf.type) ? 'storage' : 'plain'
}
/** 一串 `a && b` 本身(整串带着重定向的,见 redirectedChain,是它 body 里的那串)。 */
function chainList(node: Node): Node | undefined {
const body = node.type === 'redirected_statement' ? node.childForFieldName('body') : node
return body?.type === 'list' ? body : undefined
}
/** 一串里的各条命令,照源码顺序;整串带着重定向的往里拆(重定向算在最后一条上)。 */
function chainOperands(node: Node): Node[] {
const list = chainList(node)
return list ? named(list).filter(child => child.type !== 'comment').flatMap(chainOperands) : [node]
}
/** `a && b && c`:只用 && 连着的一串(都不是条件)。 */
function andChain(node: Node): boolean {
const operators = (list: Node): string[] => children(list).flatMap((child) => {
const inner = chainList(child)
return inner ? operators(inner) : child.isNamed ? [] : [child.type]
})
const list = chainList(node)
return !!list && operators(list).every(operator => operator === '&&') && !chainOperands(node).some(isTest)
}
/** 一步的块:整串带着的重定向算在最后一条上,它的头部延到重定向结尾。 */
function stepBlocks(operand: Node): CodeBlock[] {
const redirected = operand.type === 'command' ? redirectedChain(operand) : undefined
return statementBlocks(operand, true).map(block => redirected ? { ...block, header: { ...block.header, endIndex: redirected.endIndex, endRow: redirected.endPosition.row } } : block)
}
/**
* 被别的语言当一串步骤执行的 shell(Dockerfile 的 RUN):只有一句、是只用 && 连着的一串命令时拆开,一条命令一步(一步失败就停);
* 别的照 toBlocks 一句一步。
*/
function bashSteps(root: Node): StepBlocks {
const items = named(root)
const [only, ...more] = items.filter(item => item.type !== 'comment')
if (only && more.length === 0 && andChain(only))
return { blocks: chainOperands(only).flatMap(stepBlocks), chained: true }
return { blocks: statements(items, -1, true), chained: false }
}
/** bash / sh / zsh 脚本(VS Code 的 languageId 是 shellscript)的语言插件。@param wasmDir 语法包 .wasm 所在目录 */
export function createBashAdapter(wasmDir: string): LanguageAdapter {
return {
id: 'bash',
languageIds: ['shellscript'],
grammarPath: () => path.join(wasmDir, 'tree-sitter-bash.wasm'),
toBlocks: root => programBlocks(root),
steps: root => bashSteps(root),
tokenRole: bashTokenRole,
command: bashCommand,
symbols: root => bashSymbols(root),
displayNames: bashNameReader,
}
}