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

241 lines
13 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 { BlockView } from '../blockView'
import { describe, expect, it } from 'vitest'
import { readBlockViews } from '../blockView'
import { commandRuleLookup } from '../commandRules'
import { packageCommandRules } from '../commands/packageCommands'
import { shellCommandRules } from '../commands/shellCommands'
import { systemCommandRules } from '../commands/systemCommands'
import { createBashAdapter } from '../languages/bash'
import { flatSegments, readFixture, runtime, viewLines, WASM_DIR } from '../testSupport'
import { bashZhTranslator } from './bashZh'
const bash = createBashAdapter(WASM_DIR)
/** 内置的全部命令规则,和插件里注册的一样。 */
const RULES = [...shellCommandRules, ...systemCommandRules, ...packageCommandRules]
async function read(source: string, reports: string[] = []): Promise<BlockView[]> {
const { blocks } = await readBlockViews(runtime, bash, bashZhTranslator, 'shellscript', source, { commandRule: commandRuleLookup(RULES, text => reports.push(text)) })
return blocks
}
async function readLines(source: string): Promise<string[]> {
return viewLines(await read(source))
}
/** luffy 桌面端 fetch-runtime.sh 读出来的样子(和原型 SHELL_BLOCKS 对得上,原型略去的 95-100、116-121 行也在)。 */
const FETCH_READING = [
'interpreter 1',
' 解释器: bash',
' 怎么找: 按 PATH 去找(/usr/bin/env bash)',
'comment 2: 下载执行器自带的 git 与 python, 放到 src-tauri/resources/runtime/<target>/{git,python}。',
'options 14',
' 出错就停: 是 -e',
' 用到没设的变量就报错: 是 -u',
' 管道里任一步失败都算失败: 是 -o pipefail',
'variable 16',
' 名称: target',
' 初始值: 第 1 个参数(没给就是空)',
'variable 17',
' 名称: here',
' 初始值: 执行 〔进入 脚本所在目录/.. 然后 打印当前目录〕 的输出',
'variable 18',
' 名称: dest',
' 初始值: here/src-tauri/resources/runtime/target',
'variable 19',
' 名称: cache',
' 初始值: XDG_CACHE_HOME(没设就用 HOME/.cache)/luffy-desktop-runtime',
'const 21',
' 名称: PY_RELEASE',
' 值: 20260924',
'const 22',
' 名称: PY_VERSION',
' 值: 3.13.15',
'case 24: 按 target 分情况',
' x86_64-pc-windows-msvc | GIT_URL = https://github.com/git-for-windows/git/releases/download/v2.55.0.windows.5/MinGit-2.55.0.5-64-bit.zip ⏎ GIT_SHA256 = 56d7b226b7693196cfc71fef26568f536c4a021ab6c37ff2db4287bed908e96e ⏎ PY_SHA256 = e42fa944748a50e9ff481cbb817ef8a6e3da6fbcf0cf6f29b554e1acb8c7384d',
' aarch64-apple-darwin | GIT_URL = https://github.com/desktop/dugite-native/releases/download/v2.53.0-4/dugite-native-v2.53.0-4098283-macOS-arm64.tar.gz ⏎ GIT_SHA256 = f9dc64635a5b62fbd7ad95db73268bbb8912255ac516d65d37bf7af22fcb8ffe ⏎ PY_SHA256 = 064afb7c2fc0bbf511d886288adf98696af5105e36c138cdf2c199c0146fcf68',
' 其他 | 打印 "用法: 脚本路径 <x86_64-pc-windows-msvc|aarch64-apple-darwin>"(打印到错误输出) ⏎ 退出,退出码 2',
'variable 40',
' 名称: PY_URL',
' 初始值: https://github.com/astral-sh/python-build-standalone/releases/download/PY_RELEASE/cpython-PY_VERSION%2BPY_RELEASE-target-install_only_stripped.tar.gz',
'function 42',
' 名称: sha_ok',
' 输入参数: 2 个,按位置',
' 第 1 个 | $1',
' 第 2 个 | $2',
' 返回: 最后一句成不成功',
' if 43: 成立:文件 第 1 个参数 存在 并且 〔算 第 1 个参数 的 sha256 → 按空格切,取第 1 段〕 等于 第 2 个参数',
'function 46',
' 名称: fetch',
' 说明: 下载 (已缓存且校验通过就不重下), 回显本地文件路径',
' 输入参数: 2 个,按位置',
' 第 1 个 | url',
' 第 2 个 | sha',
' 输出: 打印 out(调用处用 $(fetch …) 拿到)',
' variable 48',
' 名称: url',
' 初始值: 第 1 个参数',
' 名称: sha',
' 初始值: 第 2 个参数',
' 名称: out',
' 初始值: 无',
' variable 49',
' 名称: out',
' 初始值: cache/〔取文件名 url(其中 %2B 全换成 +)〕',
' command 50: 建目录 cache(连同上级目录)',
' if 51: 如果 不 sha_ok out sha',
' command 52: 打印 "下载 url"(打印到错误输出)',
' command 53: 下载 url 到 out.part(失败就报错、跟着跳转走、最多重试 3 次)',
' command 54: 改名 out.part → out',
' if 56: 如果 不 sha_ok out sha',
' command 57: 打印 "sha256 不符: out"(打印到错误输出)',
' command 58: 退出,退出码 1',
' command 60: 打印 out',
'variable 63',
' 名称: git_pkg',
' 初始值: 执行 〔fetch GIT_URL GIT_SHA256〕 的输出',
'variable 64',
' 名称: py_pkg',
' 初始值: 执行 〔fetch PY_URL PY_SHA256〕 的输出',
'command 66: 删除 dest(连同里面的东西、不提示)',
'command 67: 建目录 dest/git(连同上级目录)',
'case 68: 按 git_pkg 分情况',
' 以 .zip 结尾 | 解压 zip git_pkg 到 dest/git(不打印过程)',
' 其他 | 解压 tar.gz git_pkg 到 dest/git',
'comment 72: python-build-standalone 的包里顶层目录就叫 python',
'command 73: 解压 tar.gz py_pkg 到 dest',
'comment 75: ── 去掉 Git Credential Manager ──────────────────────────────────────────────',
'case 78: 按 target 分情况',
' case 79: 情况:以 -apple-darwin 结尾',
' variable 80',
' 名称: gc',
' 初始值: dest/git/libexec/git-core',
' command 81: 删除 gc/git-credential-manager*、gc/*.dll、gc/*.dylib、gc/createdump、gc/NOTICE、gc/uninstall.sh(连同里面的东西、不提示)',
' comment 83: .NET 的各语言资源目录; git 自己的子目录只有 mergetools',
' command 84: 找出 gc 下一层的目录,除了 mergetools,逐个 删除(连同里面的东西、不提示)',
' case 86: 情况:以 -windows-msvc 结尾',
' comment 87: mingw64/bin 里 git 自己依赖的 DLL 都以 lib 开头 (外加 zlib1.dll), 其余 DLL 都属于 GCM;',
' variable 89',
' 名称: mb',
' 初始值: dest/git/mingw64/bin',
' command 90: 删除 mb/git-credential-manager.exe*、mb/libHarfBuzzSharp.dll、mb/libSkiaSharp.dll(不提示)',
' command 91: 找出 mb 这一层的 *.dll,除了 lib* 和 zlib1.dll,逐个 删除',
'comment 95: Windows 版 python 只有 python.exe; 平台脚本按 python3 这个名字再调起它时 (它的目录前置在',
'if 98: 如果 target 以 -windows-msvc 结尾',
' command 99: 复制 dest/python/python.exe 到 dest/python/python3.exe',
'comment 102: ── 去掉符号链接 ─────────────────────────────────────────────────────────────',
'if 105: 如果 target 以 -apple-darwin 结尾',
' variable 106',
' 名称: gc',
' 初始值: dest/git/libexec/git-core',
' comment 107: 内建命令的短横线别名 (git-add -> git …): git 自己调内建命令不走它们。同 git 构建选项',
' loop 109: 遍历 gc/git-* 匹配到的每个文件 f',
' if 110: 如果不是 〔f 是符号链接 并且 〔读出 f 指向的路径〕 等于 git〕,跳过这一个',
' case 111: 按 〔取文件名 f〕 分情况',
' git-upload-pack 或 git-receive-pack 或 git-upload-archive | 什么也不做',
' 其他 | 删除 f',
' comment 116: python 只留 bin/python3 这一个入口 (执行器按它找), 版本号命名的本体挪过来',
' variable 117',
' 名称: pybin',
' 初始值: dest/python/bin',
' variable 118',
' 名称: real',
' 初始值: 执行 〔读出 pybin/python3 指向的路径〕 的输出',
' command 119: 删除 pybin/python3',
' command 120: 改名 pybin/real → pybin/python3',
' command 121: 找出 dest/python 下面所有符号链接,逐个 删除',
' comment 122: 剩下的 (传输程序、git-remote-https 之类) 换成实体文件',
' pipeline 123: 找出 dest 下面所有符号链接 → 逐行读进 link',
' command 124: 复制 link 指向的实体文件 到 link.tmp',
' command 125: 改名 link.tmp → link',
'command 129: 打印 "自带运行时就绪: dest"(打印到错误输出)',
'command 130: 统计占用空间 dest/git、dest/python(按人读的单位、只看总数)(打印到错误输出)',
]
describe('shell 中文直译', () => {
it('fetch-runtime.sh 整份:解释器、选项、变量、分情况、函数(参数表、输出、返回)、命令规则、条件、遍历、管道', async () => {
expect(await readLines(readFixture('fetchRuntime.sh'))).toEqual(FETCH_READING)
})
it('变量展开:长度、去头去尾、截取、没设时、替换;特殊变量', async () => {
expect(await readLines(`echo "\${#x} \${x#p} \${x%%s} \${x:1:2} $@ $? \${x:=d} \${x:+y} \${x/a/b}"\n`)).toEqual([
'command 1: 打印 x 的长度 x(去掉开头的 p) x(去掉结尾最长的 s) x(从第 1 个字符起,取 2 个) 全部参数 上一条命令的退出码 x(没设就设成 d) y(x 设了才有) x(其中第一个 a 换成 b)',
])
expect(await readLines(`echo "\${x:?no}"\n`)).toEqual(['command 1: 打印 x(没设就报错:no)'])
})
it.each([
['[ "$a" -gt 3 ]', 'if 1: 成立:a 大于 3'],
['[[ "$x" != *.sh ]]', 'if 1: 成立:x 不以 .sh 结尾'],
['[ -f x ] && rm x', 'if 1: 如果 文件 x 存在,删除 x'],
])('条件:%s', async (source, expected) => {
expect(await readLines(`${source}\n`)).toEqual([expected])
})
it.each([
['cmd 2>/dev/null', 'cmd(丢掉错误输出)'],
['cmd >> log 2>&1', 'cmd(追加到 log)(错误输出并进输出)'],
['grep -qi error log.txt', '在 log.txt 里找含 error 的行(只看找没找到、不分大小写)'],
['sed -i \'s/a/b/g\' conf', '把 conf 里的 a 全换成 b(直接改文件)'],
['chmod +x run.sh', '给 run.sh 加上可执行权限'],
['ln -sf target link', '建符号链接 link → target(已有就替换)'],
['rm -rf --one-file-system x', '删除 x(连同里面的东西、不提示) --one-file-system'],
['foo -x "$y"', 'foo -x y'],
['mkdir -p d && cd d', '建目录 d(连同上级目录) 然后 进入 d'],
['mkdir -p d && echo hi > d/a', '建目录 d(连同上级目录) 然后 打印 "hi"(写到 d/a)'],
['curl -sSL -o f https://x', '下载 https://x 到 f(不打印进度、跟着跳转走)'],
['. /etc/os-release', '读入 /etc/os-release 里的变量和函数'],
['pg_repack --version', '打印 pg_repack 的版本'],
['cmd || exit 1', 'cmd,失败就 退出,退出码 1'],
])('命令:%s(规则不认识的选项照抄,没有规则的命令照抄命令名)', async (source, expected) => {
expect(await readLines(`${source}\n`)).toEqual([`command 1: ${expected}`])
})
it('遍历:写出来的几项、全部参数、只要 / 直到', async () => {
expect(await readLines('for x in a b; do echo "$x"; done\n')).toEqual(['loop 1: 遍历 a、b 中的每个 x', ' command 1: 打印 x'])
expect(await readLines('for a in "$@"; do shift; done\n')).toEqual(['loop 1: 遍历 全部参数 的每个 a', ' command 1: shift'])
expect(await readLines('while [ -n "$x" ]; do sleep 1; done\n')).toEqual(['loop 1: 只要 x 不为空 就一直重复', ' command 1: 等 1 秒'])
expect(await readLines('until false; do break; done\n')).toEqual(['loop 1: 直到 失败 之前一直重复', ' command 1: 跳出循环'])
})
it('变量的值:$(dirname "$0") 是脚本所在目录,整个值是 $(…) 读成「执行〔…〕的输出」,只读的是常量', async () => {
expect(await readLines('x="$(dirname "$0")"\ny=$(pwd)\nreadonly N=3\n')).toEqual([
'variable 1',
' 名称: x',
' 初始值: 脚本所在目录',
'variable 2',
' 名称: y',
' 初始值: 执行 〔打印当前目录〕 的输出',
'const 3',
' 名称: N',
' 值: 3',
])
})
it('着色:字符串里写死的部分是字符串色,展开的变量是变量色,全大写的名字是常量色', async () => {
const [block] = await read(`PY_URL="https://x/$PY_RELEASE/c-\${PY_VERSION}-$target.tgz"\n`)
const value = 'properties' in block ? block.properties.find(row => row.label === '初始值')?.value : undefined
expect(value && !('markdown' in value) && flatSegments(value).map(({ text, role }) => [text, role])).toEqual([
['https://x/', 'string'],
['PY_RELEASE', 'constant'],
['/c-', 'string'],
['PY_VERSION', 'constant'],
['-', 'string'],
['target', 'variable'],
['.tgz', 'string'],
])
expect(block.codeTokens.filter(({ role }) => role !== 'plain').map(({ text, role }) => [text, role])).toContainEqual(['PY_URL', 'constant'])
})
it('命令规则出错时照抄命令并报告,别的命令不受影响', async () => {
const reports: string[] = []
const broken = { ...shellCommandRules.find(rule => rule.commands.includes('rm'))!, read: () => {
throw new Error('坏了')
} }
const { blocks } = await readBlockViews(runtime, bash, bashZhTranslator, 'shellscript', 'rm -rf x\nmkdir d\n', {
commandRule: commandRuleLookup([broken, ...shellCommandRules], text => reports.push(text)),
})
expect(viewLines(blocks)).toEqual(['command 1: rm -rf x', 'command 2: 建目录 d'])
expect(reports).toEqual([expect.stringContaining('坏了')])
})
})