Files
superwork/vscode/src/codeReader/translators/bashZh.ts
T

589 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
})