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 = { '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 = { '-f': ['文件', '存在'], '-d': ['目录', '存在'], '-e': ['', '存在'], '-a': ['', '存在'], '-L': ['', '是符号链接'], '-h': ['', '是符号链接'], '-z': ['', '为空'], '-n': ['', '不为空'], '-r': ['', '可读'], '-w': ['', '可写'], '-x': ['', '可执行'], '-s': ['', '存在且不为空'], '-p': ['', '是管道'], '-S': ['', '是套接字'], '-t': ['文件描述符', '是终端'], '-v': ['变量', '设了值'], } const BINARY_TESTS: Record = { '-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 = { 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 = { 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, })