589 lines
26 KiB
TypeScript
589 lines
26 KiB
TypeScript
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,
|
||
})
|