✨ feat(vscode): 代码阅读支持 Shell 脚本(bash / sh / zsh):解释器、选项每个开关一行、变量展开读成中文、分情况表、函数按位置的参数 / 输出 / 返回、常用命令按命令规则读成一句、重定向、条件、遍历、管道;同一文件里的函数和变量能跳转、高亮
This commit is contained in:
1 parent
c577f38bee
commit
e96694317d
12 files changed
+2559
-3
No files matched your search
@@ -0,0 +1,588 @@
|
||||
import type { Inline, Node, PropertyItem, Segment, TokenRole, Translator } from '../contract'
|
||||
import type { NodeReader, ReaderContext } from './zhTemplate'
|
||||
import { createZhTemplateTranslator } from './zhTemplate'
|
||||
|
||||
// shell 脚本的中文直译:块头部、值、条件、命令都由这里的读法(ZhTable.readers)读,模板引擎只管拼词、着色、源码下标;
|
||||
// 命令的意思交给命令规则(SourceDocument.commandRule),这里只管 shell 自己的写法:展开、字符串、条件、通配、管道、分情况。
|
||||
|
||||
function seg(text: string, role: TokenRole): Segment {
|
||||
return { text, role }
|
||||
}
|
||||
|
||||
const word = (text: string): Segment => seg(text, 'keyword')
|
||||
const hint = (text: string): Segment => seg(text, 'hint')
|
||||
const plain = (text: string): Segment => seg(text, 'plain')
|
||||
const literal = (text: string): Segment => seg(text, 'string')
|
||||
const space = (): Segment => plain(' ')
|
||||
|
||||
function named(node: Node): Node[] {
|
||||
return node.namedChildren.filter((child): child is Node => child !== null && child.type !== 'comment')
|
||||
}
|
||||
|
||||
function joined(parts: Inline[][], separator: Inline[]): Inline[] {
|
||||
return parts.flatMap((part, i) => i > 0 ? [...separator, ...part] : part)
|
||||
}
|
||||
|
||||
/** 〔…〕括起来:一段读出来的命令、条件嵌在别的话里。 */
|
||||
function bracketed(parts: Inline[]): Inline[] {
|
||||
return [plain('〔'), ...parts, plain('〕')]
|
||||
}
|
||||
|
||||
// ─── 展开 ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** `$1`、`$0`、`$@` 这类特殊变量读成什么。 */
|
||||
const SPECIAL_ZH: Record<string, string> = {
|
||||
'0': '脚本路径',
|
||||
'@': '全部参数',
|
||||
'*': '全部参数',
|
||||
'#': '参数个数',
|
||||
'?': '上一条命令的退出码',
|
||||
'$': '当前进程号',
|
||||
'!': '最近一个后台命令的进程号',
|
||||
'-': '当前开关',
|
||||
}
|
||||
|
||||
const POSITIONAL = /^[1-9]$/
|
||||
|
||||
/** 展开里的名字:第几个参数、特殊变量读成中文,别的名字照抄(带源码下标)。 */
|
||||
function variableName(node: Node, context: ReaderContext): Inline[] {
|
||||
if (POSITIONAL.test(node.text))
|
||||
return [seg(`第 ${node.text} 个参数`, 'variable')]
|
||||
const special = SPECIAL_ZH[node.text]
|
||||
if (special && node.type === 'special_variable_name')
|
||||
return [seg(special, 'variable')]
|
||||
return [context.name(node)]
|
||||
}
|
||||
|
||||
/** 展开的值 `${x:-默认}` 里那个值:空的写「空」。 */
|
||||
function operandText(nodes: Node[], context: ReaderContext): Inline[] {
|
||||
return nodes.length > 0 ? nodes.flatMap(node => context.say(node)) : []
|
||||
}
|
||||
|
||||
/**
|
||||
* `${…}`:取值、没设时用默认值(`:-`,第几个参数写「没给」)、没设时设成(`:=`)、没设就报错(`:?`)、设了才有(`:+`)、
|
||||
* 长度(`${#x}`)、去掉开头 / 结尾(`#` `##` `%` `%%`)、替换(`/` `//`)、截取(`:偏移:长度`)、大小写。认不出的照常翻。
|
||||
*/
|
||||
const expansion: NodeReader = (node, context) => {
|
||||
const parts = node.children.filter((child): child is Node => child !== null)
|
||||
const nameAt = parts.findIndex(child => child.type === 'variable_name' || child.type === 'special_variable_name')
|
||||
if (nameAt < 0)
|
||||
return undefined
|
||||
const name = variableName(parts[nameAt], context)
|
||||
const operators = parts.filter((child, at) => node.fieldNameForChild(at) === 'operator')
|
||||
const prefix = operators.find(operator => operator.startIndex < parts[nameAt].startIndex)
|
||||
if (prefix?.type === '#')
|
||||
return [...name, space(), word('的长度')]
|
||||
const [operator, second] = operators.filter(op => op.startIndex > parts[nameAt].startIndex)
|
||||
if (!operator)
|
||||
return name
|
||||
const operands = parts.filter(child => child.isNamed && child.startIndex >= operator.endIndex && (!second || child.endIndex <= second.startIndex))
|
||||
const rest = second ? parts.filter(child => child.isNamed && child.startIndex >= second.endIndex) : []
|
||||
const value = operandText(operands, context)
|
||||
const given = POSITIONAL.test(parts[nameAt].text) ? '给' : '设'
|
||||
const note = (...inner: Inline[]): Inline[] => [...name, hint('('), ...inner, hint(')')]
|
||||
switch (operator.type) {
|
||||
case ':-':
|
||||
case '-':
|
||||
return value.length > 0 ? note(hint(`没${given}就用 `), ...value) : note(hint(`没${given}就是空`))
|
||||
case ':=':
|
||||
case '=':
|
||||
return note(hint(`没${given}就设成 `), ...value)
|
||||
case ':?':
|
||||
case '?':
|
||||
return value.length > 0 ? note(hint(`没${given}就报错:`), ...value) : note(hint(`没${given}就报错`))
|
||||
case ':+':
|
||||
case '+':
|
||||
return [...value, hint('('), ...name, hint(` ${given}了才有)`)]
|
||||
case '#':
|
||||
return note(hint('去掉开头的 '), ...value)
|
||||
case '##':
|
||||
return note(hint('去掉开头最长的 '), ...value)
|
||||
case '%':
|
||||
return note(hint('去掉结尾的 '), ...value)
|
||||
case '%%':
|
||||
return note(hint('去掉结尾最长的 '), ...value)
|
||||
case '/':
|
||||
return note(hint('其中第一个 '), ...value, hint(' 换成 '), ...operandText(rest, context))
|
||||
case '//':
|
||||
return note(hint('其中 '), ...value, hint(' 全换成 '), ...operandText(rest, context))
|
||||
case ':':
|
||||
return note(hint('从第 '), ...value, hint(' 个字符起'), ...(rest.length > 0 ? [hint(',取 '), ...operandText(rest, context), hint(' 个')] : []))
|
||||
case '^^':
|
||||
return note(hint('全转大写'))
|
||||
case ',,':
|
||||
return note(hint('全转小写'))
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const simpleExpansion: NodeReader = (node, context) => {
|
||||
const [name] = named(node)
|
||||
return name ? variableName(name, context) : undefined
|
||||
}
|
||||
|
||||
/** 整个是赋给变量的值(`x="$(…)"`、`x=$(…)`):读成「执行〔…〕的输出」。 */
|
||||
function assignedValue(substitution: Node): boolean {
|
||||
const parent = substitution.parent
|
||||
if (parent?.type === 'variable_assignment')
|
||||
return true
|
||||
return parent?.type === 'string' && named(parent).length === 1 && parent.parent?.type === 'variable_assignment'
|
||||
}
|
||||
|
||||
/**
|
||||
* `$(…)`:里面读成的话括在〔〕里;整个是赋给变量的值时写「执行〔…〕的输出」。
|
||||
* 命令规则读出来的是一个东西(`$(dirname "$0")` 是脚本所在目录)时直接写它。
|
||||
*/
|
||||
const commandSubstitution: NodeReader = (node, context) => {
|
||||
const inner = named(node)
|
||||
if (inner.length === 1) {
|
||||
const reading = context.command(inner[0])
|
||||
if (reading?.value)
|
||||
return reading.text
|
||||
}
|
||||
const text = joined(inner.map(statement => context.say(statement)), [plain(';')])
|
||||
return assignedValue(node) ? [hint('执行'), space(), ...bracketed(text), space(), hint('的输出')] : bracketed(text)
|
||||
}
|
||||
|
||||
/**
|
||||
* 双引号字符串:去掉引号,写死的部分按字符串着色,展开照常读;空的写「空字符串」。
|
||||
* 两个展开之间的空格语法树算在后一个展开里(`"$a $b"`),照源码补上。
|
||||
*/
|
||||
const string: NodeReader = (node, context) => {
|
||||
const parts = named(node)
|
||||
if (parts.length === 0)
|
||||
return [hint('空字符串')]
|
||||
const { source } = context.document
|
||||
let cursor = node.startIndex + 1
|
||||
return parts.flatMap((part) => {
|
||||
const gap = source.slice(cursor, part.startIndex) + (part.type === 'string_content' ? '' : /^\s*/.exec(part.text)?.[0] ?? '')
|
||||
cursor = part.endIndex
|
||||
const said = part.type === 'string_content' ? [literal(part.text.replace(/\\([$`"\\])/g, '$1'))] : context.say(part)
|
||||
return gap ? [literal(gap), ...said] : said
|
||||
})
|
||||
}
|
||||
|
||||
const rawString: NodeReader = (node) => {
|
||||
const text = node.text.slice(1, -1)
|
||||
return text ? [literal(text)] : [hint('空字符串')]
|
||||
}
|
||||
|
||||
const concatenation: NodeReader = (node, context) => named(node).flatMap(part => context.say(part))
|
||||
|
||||
const wordReader: NodeReader = node => [seg(node.text.replace(/\\(.)/g, '$1'), /^\d+(?:\.\d+)*$/.test(node.text) ? 'number' : 'string')]
|
||||
|
||||
// ─── 通配 ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const GLOB = /[*?[]/
|
||||
|
||||
/**
|
||||
* 一个写死的通配:`*x` 以 x 结尾、`x*` 以 x 开头、`*x*` 含有 x,别的写「匹配 …」;`*` 在分情况里是「其他」、在条件里是「任意」。
|
||||
* negate:「不以 … 结尾」这些。
|
||||
*/
|
||||
function globText(text: string, all: string, negate = false): Inline[] {
|
||||
const not = negate ? '不' : ''
|
||||
if (text === '*')
|
||||
return [hint(all)]
|
||||
const inner = text.slice(1, -1)
|
||||
if (text.startsWith('*') && text.endsWith('*') && inner && !GLOB.test(inner))
|
||||
return [word(`${negate ? '不含' : '含有'}`), space(), literal(inner)]
|
||||
const tail = text.slice(1)
|
||||
if (text.startsWith('*') && !GLOB.test(tail))
|
||||
return [word(`${not}以`), space(), literal(tail), space(), word('结尾')]
|
||||
const head = text.slice(0, -1)
|
||||
if (text.endsWith('*') && !GLOB.test(head))
|
||||
return [word(`${not}以`), space(), literal(head), space(), word('开头')]
|
||||
if (GLOB.test(text))
|
||||
return [word(`${not}匹配`), space(), literal(text)]
|
||||
return negate ? [word('不是'), space(), literal(text)] : [literal(text)]
|
||||
}
|
||||
|
||||
/** 写死的文字(没有展开):通配按 globText 读。 */
|
||||
function literalOf(node: Node): string | undefined {
|
||||
switch (node.type) {
|
||||
case 'word':
|
||||
case 'extglob_pattern':
|
||||
return node.text.replace(/\\(.)/g, '$1')
|
||||
case 'raw_string':
|
||||
return node.text.slice(1, -1)
|
||||
case 'string':
|
||||
return named(node).every(part => part.type === 'string_content') ? named(node).map(part => part.text).join('') : undefined
|
||||
case 'concatenation': {
|
||||
const parts = named(node).map(literalOf)
|
||||
return parts.every(part => part !== undefined) ? parts.join('') : undefined
|
||||
}
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** 分情况的一个情况的几个模式:`a | b` 读成「a 或 b」,`*` 是「其他」。 */
|
||||
function patternsText(item: Node, context: ReaderContext): Inline[] {
|
||||
const patterns = item.childrenForFieldName('value').filter((pattern): pattern is Node => pattern !== null)
|
||||
return joined(patterns.map((pattern) => {
|
||||
const text = literalOf(pattern)
|
||||
return text === undefined ? context.say(pattern) : globText(text, '其他')
|
||||
}), [space(), word('或'), space()])
|
||||
}
|
||||
|
||||
// ─── 条件 ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** `[ -f x ]` 这类:[前面的话, 后面的话],x 夹在中间。 */
|
||||
const UNARY_TESTS: Record<string, [string, string]> = {
|
||||
'-f': ['文件', '存在'],
|
||||
'-d': ['目录', '存在'],
|
||||
'-e': ['', '存在'],
|
||||
'-a': ['', '存在'],
|
||||
'-L': ['', '是符号链接'],
|
||||
'-h': ['', '是符号链接'],
|
||||
'-z': ['', '为空'],
|
||||
'-n': ['', '不为空'],
|
||||
'-r': ['', '可读'],
|
||||
'-w': ['', '可写'],
|
||||
'-x': ['', '可执行'],
|
||||
'-s': ['', '存在且不为空'],
|
||||
'-p': ['', '是管道'],
|
||||
'-S': ['', '是套接字'],
|
||||
'-t': ['文件描述符', '是终端'],
|
||||
'-v': ['变量', '设了值'],
|
||||
}
|
||||
|
||||
const BINARY_TESTS: Record<string, string> = {
|
||||
'-eq': '等于',
|
||||
'-ne': '不等于',
|
||||
'-lt': '小于',
|
||||
'-le': '不大于',
|
||||
'-gt': '大于',
|
||||
'-ge': '不小于',
|
||||
'-nt': '比它新',
|
||||
'-ot': '比它旧',
|
||||
'=~': '匹配正则',
|
||||
'<': '排在前面',
|
||||
'>': '排在后面',
|
||||
'&&': '并且',
|
||||
'-a': '并且',
|
||||
'||': '或者',
|
||||
'-o': '或者',
|
||||
}
|
||||
|
||||
const unaryExpression: NodeReader = (node, context) => {
|
||||
const operator = node.childForFieldName('operator')
|
||||
const [operand] = named(node).filter(child => child.startIndex >= (operator?.endIndex ?? node.startIndex))
|
||||
if (!operand)
|
||||
return undefined
|
||||
if (node.child(0)?.type === '!')
|
||||
return [word('不'), space(), ...context.say(operand)]
|
||||
const test = operator && UNARY_TESTS[operator.text]
|
||||
if (!test)
|
||||
return undefined
|
||||
const [before, after] = test
|
||||
return [...(before ? [plain(before), space()] : []), ...context.say(operand), space(), word(after)]
|
||||
}
|
||||
|
||||
/** 在 `[[ … ]]` 里(`==` 右边是通配)还是 `[ … ]` 里(`=` 是比文字)。 */
|
||||
function doubleBracket(node: Node): boolean {
|
||||
for (let parent = node.parent; parent; parent = parent.parent) {
|
||||
if (parent.type === 'test_command')
|
||||
return parent.child(0)?.type === '[['
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const binaryExpression: NodeReader = (node, context) => {
|
||||
const left = node.childForFieldName('left')
|
||||
const right = node.childForFieldName('right')
|
||||
const operator = node.childForFieldName('operator')?.text
|
||||
if (!left || !right || !operator)
|
||||
return undefined
|
||||
if (operator === '=' || operator === '==' || operator === '!=') {
|
||||
const negate = operator === '!='
|
||||
const text = doubleBracket(node) ? literalOf(right) : undefined
|
||||
const phrase = text !== undefined && GLOB.test(text)
|
||||
? globText(text, '任意', negate)
|
||||
: [word(negate ? '不等于' : '等于'), space(), ...context.say(right)]
|
||||
return [...context.say(left), space(), ...phrase]
|
||||
}
|
||||
const meaning = BINARY_TESTS[operator]
|
||||
return meaning ? [...context.say(left), space(), word(meaning), space(), ...context.say(right)] : undefined
|
||||
}
|
||||
|
||||
/** `[ … ]`:里面的条件;自己就是一句语句时前面写「成立:」(同一串全是条件的,见 list)。 */
|
||||
const testCommand: NodeReader = (node, context) => {
|
||||
const [inner] = named(node)
|
||||
return inner ? [...(isStatement(node) ? [word('成立:')] : []), ...context.say(inner)] : undefined
|
||||
}
|
||||
|
||||
const negatedCommand: NodeReader = (node, context) => {
|
||||
const [inner] = named(node)
|
||||
return inner ? [word('不'), space(), ...context.say(inner)] : undefined
|
||||
}
|
||||
|
||||
const parenthesized: NodeReader = (node, context) => {
|
||||
const [inner] = named(node)
|
||||
return inner ? bracketed(context.say(inner)) : undefined
|
||||
}
|
||||
|
||||
// ─── 一串命令、管道 ──────────────────────────────────────────────────────────
|
||||
|
||||
function isTest(node: Node): boolean {
|
||||
return node.type === 'test_command' || (node.type === 'negated_command' && named(node).some(isTest))
|
||||
}
|
||||
|
||||
/** `a && b || c`:几句和它们之间的 && / ||,照源码顺序。 */
|
||||
function listParts(node: Node): { operands: Node[], operators: string[] } {
|
||||
if (node.type !== 'list')
|
||||
return { operands: [node], operators: [] }
|
||||
const [left, right] = named(node)
|
||||
const operator = node.children.find(child => child && !child.isNamed && (child.type === '&&' || child.type === '||'))?.type ?? '&&'
|
||||
const head = left ? listParts(left) : { operands: [], operators: [] }
|
||||
const tail = right ? listParts(right) : { operands: [], operators: [] }
|
||||
return { operands: [...head.operands, ...tail.operands], operators: [...head.operators, operator, ...tail.operators] }
|
||||
}
|
||||
|
||||
/** 是条件的一部分:if / elif / while 的条件,或在 `[ … ]` 里。 */
|
||||
function inCondition(node: Node): boolean {
|
||||
for (let child = node, parent = node.parent; parent; child = parent, parent = parent.parent) {
|
||||
if (parent.type === 'test_command' || parent.type === 'command_substitution')
|
||||
return parent.type === 'test_command'
|
||||
if (parent.type === 'if_statement' || parent.type === 'while_statement' || parent.type === 'elif_clause') {
|
||||
const then = parent.children.find(token => token?.type === 'then' || token?.type === 'do_group')
|
||||
return !!then && child.endIndex <= then.startIndex
|
||||
}
|
||||
if (parent.type !== 'list' && parent.type !== 'negated_command')
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** 自己就是一句语句(不在条件、命令替换、别的一串命令里)。 */
|
||||
function isStatement(node: Node): boolean {
|
||||
const parent = node.parent
|
||||
return !!parent && parent.type !== 'list' && parent.type !== 'command_substitution' && !inCondition(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* 一串命令:
|
||||
* - 全是条件:「A 并且 B」,自己就是一句语句时前面写「成立:」(这句的结果就是条件成不成立);
|
||||
* - 前面全是条件、最后跟一句:「如果〔…〕,<那一句>」,用 || 接的是「如果不是〔…〕,…」(`[ … ] || continue` 跳过这一个);
|
||||
* - 别的:在条件里用「并且 / 或者」,否则「然后 / ,失败就」。
|
||||
*/
|
||||
const list: NodeReader = (node, context) => {
|
||||
const { operands, operators } = listParts(node)
|
||||
const conditional = inCondition(node) || operands.every(isTest)
|
||||
const chain = (count: number, forCondition: boolean): Inline[] => operands.slice(0, count).flatMap((operand, i) => {
|
||||
const said = context.say(operand)
|
||||
if (i === 0)
|
||||
return said
|
||||
const joiner = operators[i - 1] === '&&' ? (forCondition ? '并且' : '然后') : (forCondition ? '或者' : ',失败就')
|
||||
return [...(joiner.startsWith(',') ? [] : [space()]), word(joiner), space(), ...said]
|
||||
})
|
||||
if (operands.every(isTest))
|
||||
return [...(isStatement(node) ? [word('成立:')] : []), ...chain(operands.length, true)]
|
||||
const last = operands[operands.length - 1]
|
||||
if (!conditional && operands.slice(0, -1).every(isTest) && operands.length > 1) {
|
||||
const condition = chain(operands.length - 1, true)
|
||||
const negated = operators[operators.length - 1] === '||'
|
||||
return [word(negated ? '如果不是' : '如果'), space(), ...(operands.length > 2 ? bracketed(condition) : condition), word(','), ...context.say(last)]
|
||||
}
|
||||
return chain(operands.length, conditional)
|
||||
}
|
||||
|
||||
/** 管道:每一段读出来,用「→」连起来;最后一段是循环时只写它的头(逐行读进 x)。 */
|
||||
const pipeline: NodeReader = (node, context) => joined(named(node).map(part => context.say(part)), [space(), word('→'), space()])
|
||||
|
||||
const redirectedStatement: NodeReader = (node, context) => {
|
||||
const body = node.childForFieldName('body')
|
||||
return body && body.type !== 'command' ? context.say(body) : undefined
|
||||
}
|
||||
|
||||
// ─── 赋值 ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** 写在表格里、条件后面的一句赋值:「名字 = 值」。 */
|
||||
const variableAssignment: NodeReader = (node, context) => {
|
||||
const name = node.childForFieldName('name')
|
||||
const value = node.childForFieldName('value')
|
||||
if (!name)
|
||||
return undefined
|
||||
return [context.name(name), plain(' = '), ...(value ? context.say(value) : [hint('空')])]
|
||||
}
|
||||
|
||||
const DECLARATIONS_ZH: Record<string, string> = { local: '局部', export: '导出', readonly: '只读', declare: '声明', typeset: '声明' }
|
||||
|
||||
const declarationCommand: NodeReader = (node, context) => {
|
||||
const keyword = node.child(0)?.type ?? ''
|
||||
const declared = named(node).filter(child => child.type === 'variable_assignment' || child.type === 'variable_name')
|
||||
return [seg(DECLARATIONS_ZH[keyword] ?? keyword, 'storage'), space(), ...joined(declared.map(item => context.say(item)), [plain(',')])]
|
||||
}
|
||||
|
||||
const variableNameReader: NodeReader = (node, context) => variableName(node, context)
|
||||
|
||||
// ─── 复合语句的头部 ──────────────────────────────────────────────────────────
|
||||
|
||||
/** if / elif / while 的条件:then / do 之前的各句(去掉分号)。 */
|
||||
function conditions(node: Node): Node[] {
|
||||
const stop = node.children.find(child => child?.type === 'then' || child?.type === 'do_group')
|
||||
return named(node).filter(child => !stop || child.endIndex <= stop.startIndex)
|
||||
}
|
||||
|
||||
function conditionText(node: Node, context: ReaderContext): Inline[] {
|
||||
return joined(conditions(node).map(condition => context.say(condition)), [plain(';')])
|
||||
}
|
||||
|
||||
const ifStatement: NodeReader = (node, context) => [word('如果'), space(), ...conditionText(node, context)]
|
||||
const elifClause: NodeReader = (node, context) => [word('否则如果'), space(), ...conditionText(node, context)]
|
||||
const elseClause: NodeReader = () => [word('否则')]
|
||||
|
||||
/** for 的来源里有通配(`"$gc"/git-*`):逐个取匹配到的文件。 */
|
||||
function hasGlob(node: Node): boolean {
|
||||
if (node.type === 'word')
|
||||
return GLOB.test(node.text)
|
||||
if (node.type === 'string' || node.type === 'raw_string' || node.type === 'simple_expansion' || node.type === 'expansion')
|
||||
return false
|
||||
return named(node).some(hasGlob)
|
||||
}
|
||||
|
||||
/**
|
||||
* for:「遍历 <通配> 匹配到的每个文件 f」「遍历 a、b 中的每个 x」,没写 in 的是「遍历 全部参数 的每个 x」。
|
||||
*/
|
||||
const forStatement: NodeReader = (node, context) => {
|
||||
const variable = node.childForFieldName('variable')
|
||||
const values = node.childrenForFieldName('value').filter((value): value is Node => value !== null)
|
||||
if (!variable)
|
||||
return undefined
|
||||
const name = context.name(variable)
|
||||
if (values.length === 0)
|
||||
return [word('遍历'), space(), hint('全部参数'), space(), word('的每个'), space(), name]
|
||||
if (values.length === 1 && hasGlob(values[0]))
|
||||
return [word('遍历'), space(), ...context.say(values[0]), space(), hint('匹配到的每个文件'), space(), name]
|
||||
const source = joined(values.map(value => context.say(value)), [plain('、')])
|
||||
return [word('遍历'), space(), ...source, space(), word(values.length > 1 ? '中的每个' : '的每个'), space(), name]
|
||||
}
|
||||
|
||||
/** while:条件是 `read x` 时「逐行读进 x」;until 是「直到 … 之前一直重复」,别的「只要 … 就一直重复」。 */
|
||||
const whileStatement: NodeReader = (node, context) => {
|
||||
const [condition] = conditions(node)
|
||||
if (!condition)
|
||||
return undefined
|
||||
const until = node.child(0)?.type === 'until'
|
||||
if (!until && condition.type === 'command' && condition.childForFieldName('name')?.text === 'read') {
|
||||
const targets = condition.childrenForFieldName('argument').filter((arg): arg is Node => arg !== null && arg.type === 'word' && !arg.text.startsWith('-'))
|
||||
return [word('逐行读进'), space(), ...joined(targets.map(target => [context.name(target, 'variable')]), [plain('、')])]
|
||||
}
|
||||
const text = conditionText(node, context)
|
||||
return until ? [word('直到'), space(), ...text, space(), word('之前一直重复')] : [word('只要'), space(), ...text, space(), word('就一直重复')]
|
||||
}
|
||||
|
||||
// ─── 分情况 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function caseItems(node: Node): Node[] {
|
||||
return named(node).filter(child => child.type === 'case_item')
|
||||
}
|
||||
|
||||
/** 一个情况要做的各句:`)` 后面的。 */
|
||||
function caseBody(item: Node): Node[] {
|
||||
const close = item.children.find(child => child?.type === ')')
|
||||
return named(item).filter(child => !close || child.startIndex >= close.endIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* case:「按 x 分情况」;整条语句都在这一块里时(几个情况都短),各个情况排成一张表「情况 | 做什么」,
|
||||
* 做什么是那几句,一句一行,什么都不做的写「什么也不做」。
|
||||
*/
|
||||
const caseStatement: NodeReader = (node, context) => {
|
||||
const subject = node.childForFieldName('value')
|
||||
if (!subject)
|
||||
return undefined
|
||||
const text = [word('按'), space(), ...context.say(subject), space(), word('分情况')]
|
||||
if (!context.whole(node))
|
||||
return text
|
||||
const items = caseItems(node).map((item): PropertyItem => {
|
||||
const body = caseBody(item)
|
||||
return {
|
||||
cells: [
|
||||
patternsText(item, context),
|
||||
body.length > 0 ? joined(body.map(statement => context.say(statement)), [plain('\n')]) : [hint('什么也不做')],
|
||||
],
|
||||
}
|
||||
})
|
||||
return { text, table: { columns: ['情况', '做什么'], items } }
|
||||
}
|
||||
|
||||
/** 分情况拆成子块时的一个情况:「情况:以 -apple-darwin 结尾」。 */
|
||||
const caseItem: NodeReader = (node, context) => [word('情况:'), ...patternsText(node, context)]
|
||||
|
||||
/** bash 自己的开关(set -e / set -o …)的意思。 */
|
||||
const SWITCHES_ZH: Record<string, string> = {
|
||||
errexit: '出错就停',
|
||||
nounset: '用到没设的变量就报错',
|
||||
pipefail: '管道里任一步失败都算失败',
|
||||
xtrace: '打印执行的每条命令',
|
||||
verbose: '打印读到的每一行',
|
||||
noglob: '不展开通配',
|
||||
noexec: '只检查不执行',
|
||||
allexport: '设的变量都导出',
|
||||
noclobber: '不覆盖已有文件',
|
||||
errtrace: '函数里出错也触发 ERR',
|
||||
functrace: '函数里也触发 DEBUG',
|
||||
monitor: '打开作业控制',
|
||||
notify: '后台任务结束马上通知',
|
||||
hashall: '记住命令的位置',
|
||||
braceexpand: '展开花括号',
|
||||
histexpand: '展开历史',
|
||||
physical: 'cd 走真实路径',
|
||||
keyword: '赋值写在哪都算',
|
||||
onecmd: '执行一条就退出',
|
||||
posix: '按 POSIX 行事',
|
||||
}
|
||||
|
||||
/** `#` 注释去掉井号。 */
|
||||
function commentText(_node: Node, text: string): string {
|
||||
return text.replace(/^#+\s?/, '')
|
||||
}
|
||||
|
||||
export const bashZhTranslator: Translator = createZhTemplateTranslator({
|
||||
id: 'shellscript.zh-template',
|
||||
label: '中文直译',
|
||||
languageIds: ['shellscript'],
|
||||
keywords: {},
|
||||
literals: {},
|
||||
builtinTypes: {},
|
||||
isTypePosition: () => false,
|
||||
operators: {},
|
||||
operatorParents: [],
|
||||
emptyLiterals: {},
|
||||
mapLiterals: {},
|
||||
stringNodes: [],
|
||||
commentText,
|
||||
nodeTemplates: {},
|
||||
readers: {
|
||||
string,
|
||||
raw_string: rawString,
|
||||
concatenation,
|
||||
word: wordReader,
|
||||
extglob_pattern: wordReader,
|
||||
simple_expansion: simpleExpansion,
|
||||
expansion,
|
||||
command_substitution: commandSubstitution,
|
||||
variable_name: variableNameReader,
|
||||
special_variable_name: variableNameReader,
|
||||
variable_assignment: variableAssignment,
|
||||
declaration_command: declarationCommand,
|
||||
test_command: testCommand,
|
||||
unary_expression: unaryExpression,
|
||||
binary_expression: binaryExpression,
|
||||
parenthesized_expression: parenthesized,
|
||||
negated_command: negatedCommand,
|
||||
list,
|
||||
pipeline,
|
||||
redirected_statement: redirectedStatement,
|
||||
if_statement: ifStatement,
|
||||
elif_clause: elifClause,
|
||||
else_clause: elseClause,
|
||||
for_statement: forStatement,
|
||||
while_statement: whileStatement,
|
||||
case_statement: caseStatement,
|
||||
case_item: caseItem,
|
||||
},
|
||||
switches: SWITCHES_ZH,
|
||||
})
|
||||
Reference in new issue
Block a user