import type { CommandContext, CommandRule, CommandWord, Inline } from '../contract' import type { Options } from './reading' import { hint, joined, literalText, notes, operandsText, parseOptions, plain, shellCommand, space, valueText, verb, withUnknown, word } from './reading' /** 「动作 东西(选项的意思)」这一种最常见的读法。 */ function action(name: string, options: Options, context: CommandContext, meanings: Inline[][]): Inline[] { const target = options.operands.length > 0 ? [space(), ...operandsText(options.operands, context)] : [] return withUnknown([verb(name), ...target, ...notes(meanings)], options) } /** 按 order 的顺序写出开了的开关的意思。 */ function meanings(options: Options, order: [string, string][]): Inline[][] { return order.filter(([flag]) => options.flags.has(flag)).map(([, meaning]) => [hint(meaning)]) } /** 「A 到 B」:几个来源、一个去处。 */ function toTarget(sources: readonly CommandWord[], target: CommandWord | undefined, context: CommandContext): Inline[] { const from = operandsText(sources, context) return target ? [...from, space(), word('到'), space(), ...context.translate(target.node)] : from } const rm = shellCommand('rm', ['rm'], (command, context) => { const options = parseOptions(command.arguments, { flags: 'rRfivd', long: { recursive: 'r', force: 'f', interactive: 'i', verbose: 'v', dir: 'd' } }) return action('删除', options, context, meanings(options, [['r', '连同里面的东西'], ['R', '连同里面的东西'], ['d', '也删空目录'], ['f', '不提示'], ['i', '逐个确认'], ['v', '列出删了什么']])) }) const mkdir = shellCommand('mkdir', ['mkdir'], (command, context) => { const options = parseOptions(command.arguments, { flags: 'pv', valued: 'm', long: { parents: 'p', verbose: 'v', mode: 'm' } }) const mode = options.values.get('m') return action('建目录', options, context, [ ...meanings(options, [['p', '连同上级目录']]), ...(mode ? [[hint('权限 '), ...valueText(mode, context)]] : []), ...meanings(options, [['v', '列出建了什么']]), ]) }) const curl = shellCommand('curl', ['curl'], (command, context) => { const options = parseOptions(command.arguments, { flags: 'fLsSOkIvi', valued: 'oXHdumA', long: { 'fail': 'f', 'location': 'L', 'silent': 's', 'show-error': 'S', 'remote-name': 'O', 'insecure': 'k', 'head': 'I', 'verbose': 'v', 'output': 'o', 'request': 'X', 'header': 'H', 'data': 'd', 'user': 'u', 'max-time': 'm', 'user-agent': 'A', 'retry': 'retry' }, longValued: ['retry'], }) const output = options.values.get('o') const urls = operandsText(options.operands, context) const head: Inline[] = output ? [verb('下载'), space(), ...urls, space(), word('到'), space(), ...valueText(output, context)] : [verb(options.flags.has('O') ? '下载' : '请求'), space(), ...urls] const valued = (key: string, before: string, after = ''): Inline[][] => { const value = options.values.get(key) return value ? [[hint(before), ...valueText(value, context), ...(after ? [hint(after)] : [])]] : [] } return withUnknown([...head, ...notes([ ...meanings(options, [['f', '失败就报错'], ['L', '跟着跳转走']]), ...valued('retry', '最多重试 ', ' 次'), ...meanings(options, [['O', '存成网址里的文件名'], ['s', '不显示进度'], ['S', '出错时显示错误'], ['k', '不校验证书'], ['I', '只取响应头'], ['v', '打印通信过程'], ['i', '连响应头一起打印']]), ...valued('X', '用 ', ' 方法'), ...valued('H', '带请求头 '), ...valued('d', '发送 '), ...valued('u', '用户 '), ...valued('A', '标成 '), ...valued('m', '最多等 ', ' 秒'), ])], options) }) /** tar 的压缩格式:z → tar.gz,j → tar.bz2,J → tar.xz,a 按文件名定。 */ function tarFormat(options: Options): string { if (options.flags.has('z')) return 'tar.gz' if (options.flags.has('j')) return 'tar.bz2' return options.flags.has('J') ? 'tar.xz' : 'tar' } const tar = shellCommand('tar', ['tar'], (command, context) => { const options = parseOptions(command.arguments, { flags: 'xctzjJvap', valued: 'fC', long: { 'extract': 'x', 'create': 'c', 'list': 't', 'gzip': 'z', 'bzip2': 'j', 'xz': 'J', 'verbose': 'v', 'file': 'f', 'directory': 'C', 'auto-compress': 'a' }, bare: true, }) const file = options.values.get('f') const dir = options.values.get('C') const archive = [literalText(tarFormat(options)), ...(file ? [space(), ...valueText(file, context)] : [])] const into = dir ? [space(), word('到'), space(), ...valueText(dir, context)] : [] const extra = notes(meanings(options, [['v', '列出每个文件'], ['p', '保留权限']])) if (options.flags.has('x')) return withUnknown([verb('解压'), space(), ...archive, ...into, ...operandsTail(options, context, '只要 '), ...extra], options) if (options.flags.has('c')) return withUnknown([verb('打包'), space(), ...operandsText(options.operands, context), space(), word('成'), space(), ...archive, ...extra], options) if (options.flags.has('t')) return withUnknown([verb('列出'), space(), ...archive, space(), word('里的文件'), ...extra], options) return undefined }) /** 解压时写在后面的几个成员:只解出它们。 */ function operandsTail(options: Options, context: CommandContext, lead: string): Inline[] { return options.operands.length > 0 ? [hint(`(${lead}`), ...operandsText(options.operands, context), hint(')')] : [] } const unzip = shellCommand('unzip', ['unzip'], (command, context) => { const options = parseOptions(command.arguments, { flags: 'qonl', valued: 'd' }) const [archive, ...members] = options.operands if (!archive) return undefined const dir = options.values.get('d') const zip = [literalText('zip'), space(), ...context.translate(archive.node)] if (options.flags.has('l')) return withUnknown([verb('列出'), space(), ...zip, space(), word('里的文件')], options) return withUnknown([ verb('解压'), space(), ...zip, ...(dir ? [space(), word('到'), space(), ...valueText(dir, context)] : []), ...(members.length > 0 ? [hint('(只要 '), ...operandsText(members, context), hint(')')] : []), ...notes(meanings(options, [['q', '不打印过程'], ['o', '覆盖已有的不问'], ['n', '已有的不覆盖']])), ], options) }) const FIND_TYPES: Record = { f: '文件', d: '目录', l: '符号链接' } /** * find 路径 [条件…] [动作]:找出 路径 下一层的 / 这一层的 / 下面所有 东西,名字按 -name 写,`! -name x` 写「除了 x」, * -delete / -exec 命令 {} + 写「,逐个 <命令>」。认不出的条件照抄。 */ const find = shellCommand('find', ['find'], (command, context) => { const args = command.arguments const isPredicate = (arg: CommandWord): boolean => arg.literal !== undefined && /^[-!(]/.test(arg.literal) const paths: CommandWord[] = [] let i = 0 while (i < args.length && !isPredicate(args[i])) { paths.push(args[i]) i++ } let minDepth: string | undefined let maxDepth: string | undefined let type: string | undefined const names: Inline[][] = [] const excluded: Inline[][] = [] const unknown: CommandWord[] = [] let act: Inline[] | undefined let negate = false for (; i < args.length; i++) { const text = args[i].literal const value = args[i + 1] switch (text) { case '!': case '-not': negate = true continue case '-mindepth': minDepth = value?.literal i++ break case '-maxdepth': maxDepth = value?.literal i++ break case '-type': type = value?.literal i++ break case '-name': case '-iname': if (value) (negate ? excluded : names).push(context.translate(value.node)) i++ break case '-delete': act = [verb('删除')] break case '-exec': case '-execdir': { const end = args.findIndex((arg, at) => at > i && (arg.literal === '+' || arg.literal === ';')) const words = args.slice(i + 1, end < 0 ? args.length : end).filter(arg => arg.literal !== '{}') const [name, ...rest] = words const reading = name ? context.command(name, rest) : undefined act = reading ? reading.text : words.flatMap((arg, at) => [...(at > 0 ? [space()] : []), plain(arg.node.text)]) i = end < 0 ? args.length : end break } case '-print': break default: unknown.push(args[i]) } negate = false } const scope = minDepth === '1' && maxDepth === '1' ? '下一层的' : maxDepth === '1' ? '这一层的' : maxDepth ? `往下 ${maxDepth} 层以内的` : '下面所有' const what: Inline[] = names.length > 0 ? joined(names, word('或')) : [hint(type ? FIND_TYPES[type] ?? type : '东西')] const typed = names.length > 0 && type ? [hint(FIND_TYPES[type] ?? type), space()] : [] const found: Inline[] = [ verb('找出'), space(), ...operandsText(paths, context), space(), hint(scope), ...(names.length > 0 ? [space()] : []), ...typed, ...what, ...(excluded.length > 0 ? [word(',除了'), space(), ...excluded.flatMap((part, at) => at > 0 ? [space(), word('和'), space(), ...part] : part)] : []), ...(act ? [word(',逐个'), space(), ...act] : []), ] return unknown.length > 0 ? [...found, ...unknown.flatMap(arg => [space(), plain(arg.node.text)])] : found }) const cp = shellCommand('cp', ['cp'], (command, context) => { const options = parseOptions(command.arguments, { flags: 'rRafpLPvni', long: { 'recursive': 'r', 'archive': 'a', 'force': 'f', 'dereference': 'L', 'no-dereference': 'P', 'verbose': 'v', 'no-clobber': 'n', 'interactive': 'i' } }) const sources = options.operands.slice(0, -1) const target = options.operands.at(-1) if (!target || sources.length === 0) return undefined const from = options.flags.has('L') ? joined(sources.map(source => [...context.translate(source.node), space(), word('指向的实体文件')]), plain('、')) : operandsText(sources, context) return withUnknown([verb('复制'), space(), ...from, space(), word('到'), space(), ...context.translate(target.node), ...notes(meanings(options, [ ['r', '连同里面的东西'], ['R', '连同里面的东西'], ['a', '原样保留,连同里面的东西'], ['p', '保留权限和时间'], ['f', '不提示'], ['n', '已有的不覆盖'], ['i', '逐个确认'], ['v', '列出复制了什么'], ]))], options) }) const mv = shellCommand('mv', ['mv'], (command, context) => { const options = parseOptions(command.arguments, { flags: 'finv', long: { 'force': 'f', 'interactive': 'i', 'no-clobber': 'n', 'verbose': 'v' } }) const sources = options.operands.slice(0, -1) const target = options.operands.at(-1) if (!target || sources.length === 0) return undefined const moved = sources.length === 1 ? [verb('改名'), space(), ...context.translate(sources[0].node), space(), word('→'), space(), ...context.translate(target.node)] : [verb('移动'), space(), ...toTarget(sources, target, context)] return withUnknown([...moved, ...notes(meanings(options, [['f', '不提示'], ['n', '已有的不覆盖'], ['i', '逐个确认'], ['v', '列出移动了什么']]))], options) }) /** 要打印的东西:有写死的文字时连引号照抄(打印 "下载 url"),只是一个变量时不带引号(打印 out)。 */ function printed(operands: readonly CommandWord[], context: CommandContext): Inline[] { const parts = joined(operands.map(operand => context.translate(operand.node)), literalText(' ')) const quoted = operands.some(operand => operand.literal !== undefined || (operand.node.type === 'string' && operand.node.namedChildren.some(child => child?.type === 'string_content'))) return quoted ? [literalText('"'), ...parts, literalText('"')] : parts } const echo = shellCommand('echo', ['echo'], (command, context) => { const args = [...command.arguments] const flags = new Set() while (args[0]?.literal && /^-[neE]+$/.test(args[0].literal)) { for (const flag of args[0].literal.slice(1)) flags.add(flag) args.shift() } return [verb('打印'), ...(args.length > 0 ? [space(), ...printed(args, context)] : [hint('空行')]), ...notes([ ...(flags.has('n') ? [[hint('不换行')]] : []), ...(flags.has('e') ? [[hint('认转义')]] : []), ])] }) const printf = shellCommand('printf', ['printf'], (command, context) => { const [format, ...args] = command.arguments if (!format || format.literal?.startsWith('-')) return undefined return [verb('按格式打印'), space(), ...printed([format], context), ...(args.length > 0 ? [word(',填入'), space(), ...operandsText(args, context)] : [])] }) const exit = shellCommand('exit', ['exit'], (command, context) => { const [code] = command.arguments return [verb('退出'), ...(code ? [hint(',退出码 '), ...context.translate(code.node)] : [])] }) const returns = shellCommand('return', ['return'], (command, context) => { const [code] = command.arguments return [verb('返回'), ...(code ? [space(), ...context.translate(code.node)] : [])] }) const cd = shellCommand('cd', ['cd'], (command, context) => { const options = parseOptions(command.arguments, { flags: 'LP' }) const [target] = options.operands if (!target) return [verb('进入主目录')] if (target.literal === '-') return [verb('回到上一个目录')] return withUnknown([verb('进入'), space(), ...context.translate(target.node)], options) }) const pwd = shellCommand('pwd', ['pwd'], () => [verb('打印当前目录')]) const basename = shellCommand('basename', ['basename'], (command, context) => { const options = parseOptions(command.arguments, { flags: 'a', valued: 's', long: { suffix: 's', multiple: 'a' } }) const [path, suffix] = options.operands if (!path) return undefined const cut = options.values.get('s') ?? (suffix && !options.flags.has('a') ? { word: suffix, text: suffix.literal ?? suffix.node.text } : undefined) const paths = options.flags.has('a') ? options.operands : [path] return withUnknown([verb('取文件名'), space(), ...operandsText(paths, context), ...(cut ? [hint('(去掉 '), ...valueText(cut, context), hint(')')] : [])], options) }) /** `$0`、`"$0"`:脚本自己的路径。 */ function scriptPath(arg: CommandWord): boolean { return arg.node.text === '$0' || arg.node.text === '"$0"' } const dirname = shellCommand('dirname', ['dirname'], (command, context) => { const options = parseOptions(command.arguments, {}) const [path] = options.operands if (!path) return undefined if (options.operands.length === 1 && scriptPath(path)) return { text: [plain('脚本所在目录')], value: true } return withUnknown([verb('取所在目录'), space(), ...operandsText(options.operands, context)], options) }) const readlink = shellCommand('readlink', ['readlink'], (command, context) => { const options = parseOptions(command.arguments, { flags: 'femnq', long: { 'canonicalize': 'f', 'canonicalize-existing': 'e', 'canonicalize-missing': 'm', 'no-newline': 'n', 'quiet': 'q' } }) const [path] = options.operands if (!path) return undefined if (['f', 'e', 'm'].some(flag => options.flags.has(flag))) return withUnknown([verb('取'), space(), ...context.translate(path.node), space(), word('的真实路径')], options) return withUnknown([verb('读出'), space(), ...context.translate(path.node), space(), word('指向的路径')], options) }) /** shasum -a 256、sha256sum 这类:算 x 的 sha256;-c 是按清单校验。 */ function digest(algorithm: (options: Options) => string) { return (command: { arguments: CommandWord[] }, context: CommandContext): Inline[] | undefined => { const options = parseOptions(command.arguments, { flags: 'cbt', valued: 'a', long: { check: 'c', algorithm: 'a', binary: 'b', text: 't' } }) const name = algorithm(options) if (options.flags.has('c')) return withUnknown([verb('按清单校验'), space(), ...operandsText(options.operands, context), hint(`(${name})`)], options) return withUnknown([verb('算'), space(), ...operandsText(options.operands, context), space(), word(`的 ${name}`)], options) } } const shasum = shellCommand('shasum', ['shasum'], digest(options => `sha${options.values.get('a')?.text ?? '1'}`)) const sums = [ shellCommand('sha256sum', ['sha256sum'], digest(() => 'sha256')), shellCommand('sha1sum', ['sha1sum'], digest(() => 'sha1')), shellCommand('sha512sum', ['sha512sum'], digest(() => 'sha512')), shellCommand('md5sum', ['md5sum'], digest(() => 'md5')), ] /** cut -d 的分隔符叫什么:空格、制表符,别的照抄。 */ function delimiterName(text: string): Inline { if (text === ' ') return hint('空格') return text === '\t' ? hint('制表符') : literalText(text) } /** cut -f / -c 的范围:1 → 第 1,1,3 → 第 1、3,2- → 第 2 往后,1-3 → 第 1 到 3。 */ function rangeText(text: string, unit: string): string { const parts = text.split(',').map((part) => { const [from, to] = part.split('-') if (!part.includes('-')) return from if (!to) return `${from} ${unit}往后` return from ? `${from} 到 ${to}` : `开头到 ${to}` }) const last = parts.at(-1) ?? '' return `第 ${parts.join('、')}${last.endsWith('往后') ? '' : ` ${unit}`}` } const cut = shellCommand('cut', ['cut'], (command, context) => { const options = parseOptions(command.arguments, { flags: 's', valued: 'dfcb', long: { 'delimiter': 'd', 'fields': 'f', 'characters': 'c', 'bytes': 'b', 'only-delimited': 's' } }) const delimiter = options.values.get('d') const fields = options.values.get('f') const characters = options.values.get('c') ?? options.values.get('b') const files = options.operands.length > 0 ? operandsText(options.operands, context) : undefined if (fields) { const by = delimiter ? delimiterName(delimiter.text) : hint('制表符') const lines = files ? [space(), ...files, space(), word('的每一行')] : [] return withUnknown([verb('按'), by, verb('切'), ...lines, word(','), verb(`取${rangeText(fields.text, '段')}`)], options) } if (characters) { const range = rangeText(characters.text, '个字符') return withUnknown(files ? [verb('取'), space(), ...files, space(), word('每一行的'), verb(range)] : [verb(`取${range}`)], options) } return undefined }) const du = shellCommand('du', ['du'], (command, context) => { const options = parseOptions(command.arguments, { flags: 'shackm', valued: 'd', long: { 'summarize': 's', 'human-readable': 'h', 'all': 'a', 'total': 'c', 'max-depth': 'd' } }) const depth = options.values.get('d') return action('统计占用空间', options, context, [ ...meanings(options, [['h', '按人读的单位'], ['s', '只看总数'], ['a', '连文件也列'], ['c', '再算合计']]), ...(depth ? [[hint('往下看 '), ...valueText(depth, context), hint(' 层')]] : []), ]) }) const cat = shellCommand('cat', ['cat'], (command, context) => { const options = parseOptions(command.arguments, { flags: 'nbsvAE', long: { number: 'n' } }) const note = notes(meanings(options, [['n', '带行号'], ['b', '非空行带行号'], ['s', '连续空行只留一行']])) if (options.operands.length === 0) return withUnknown([verb('打印输入'), ...note], options) if (options.operands.length === 1) return withUnknown([verb('打印'), space(), ...operandsText(options.operands, context), space(), word('的内容'), ...note], options) return withUnknown([verb('连起来打印'), space(), ...operandsText(options.operands, context), ...note], options) }) const grep = shellCommand('grep', ['grep', 'egrep', 'fgrep'], (command, context) => { const options = parseOptions(command.arguments, { flags: 'qivrREFnoclwxsHh', valued: 'ef', long: { 'quiet': 'q', 'ignore-case': 'i', 'invert-match': 'v', 'recursive': 'r', 'extended-regexp': 'E', 'fixed-strings': 'F', 'line-number': 'n', 'only-matching': 'o', 'count': 'c', 'files-with-matches': 'l', 'word-regexp': 'w', 'line-regexp': 'x', 'regexp': 'e', 'file': 'f' }, }) const explicit = options.values.get('e') const operands = [...options.operands] const pattern: Inline[] | undefined = explicit ? valueText(explicit, context) : operands.length > 0 ? context.translate((operands.shift() as CommandWord).node) : undefined if (!pattern) return undefined const where = operands.length > 0 ? operandsText(operands, context) : [hint('输入')] return withUnknown([ verb('在'), space(), ...where, space(), verb(options.flags.has('v') ? '里找不含' : '里找含'), space(), ...pattern, space(), word('的行'), ...notes(meanings(options, [ ['q', '只看找没找到'], ['i', '不分大小写'], ['w', '整词'], ['x', '整行'], ['E', '按扩展正则'], ['F', '按原文'], ['r', '连同子目录'], ['R', '连同子目录'], ['n', '带行号'], ['o', '只打印匹配到的部分'], ['c', '只数有几行'], ['l', '只列文件名'], ['s', '不报读不了的文件'], ])), ], options) }) /** sed 的 s 命令:s/旧/新/g(分隔符可以换)。 */ function substitution(script: string): { from: string, to: string, all: boolean } | undefined { if (!script.startsWith('s') || script.length < 4) return undefined const separator = script[1] const parts = script.slice(2).split(separator) if (parts.length !== 3) return undefined const [from, to, flags] = parts return /^[gI]*$/.test(flags) ? { from, to, all: flags.includes('g') } : undefined } const sed = shellCommand('sed', ['sed'], (command, context) => { const options = parseOptions(command.arguments, { flags: 'nEri', valued: 'ef', long: { 'quiet': 'n', 'silent': 'n', 'regexp-extended': 'E', 'in-place': 'i', 'expression': 'e', 'file': 'f' } }) const operands = [...options.operands] const script = options.values.get('e') ?? (operands.length > 0 ? { word: operands[0], text: operands.shift()?.literal ?? '' } : undefined) if (!script) return undefined const files = operands.length > 0 ? operandsText(operands, context) : [hint('输入')] const note = notes(meanings(options, [['i', '直接改文件'], ['n', '不自动打印'], ['E', '按扩展正则'], ['r', '按扩展正则']])) const replaced = substitution(script.text) if (replaced) { return withUnknown([ verb('把'), space(), ...files, space(), word('里的'), space(), literalText(replaced.from), space(), verb(replaced.all ? '全换成' : '换成'), space(), literalText(replaced.to), ...note, ], options) } return withUnknown([verb('按 sed 脚本'), space(), ...valueText(script, context), space(), verb('改'), space(), ...files, ...note], options) }) const chmod = shellCommand('chmod', ['chmod'], (command, context) => { const options = parseOptions(command.arguments, { flags: 'Rv', long: { recursive: 'R', verbose: 'v' } }) const [mode, ...files] = options.operands if (!mode || files.length === 0) return undefined const note = notes(meanings(options, [['R', '连同里面的东西']])) const targets = operandsText(files, context) if (mode.literal === '+x' || mode.literal === 'a+x') return withUnknown([verb('给'), space(), ...targets, space(), verb('加上可执行权限'), ...note], options) if (mode.literal && /^[0-7]{3,4}$/.test(mode.literal)) return withUnknown([verb('把'), space(), ...targets, space(), verb('的权限改成'), space(), literalText(mode.literal), ...note], options) return withUnknown([verb('改'), space(), ...targets, space(), verb('的权限:'), ...context.translate(mode.node), ...note], options) }) const ln = shellCommand('ln', ['ln'], (command, context) => { const options = parseOptions(command.arguments, { flags: 'sfnvr', long: { 'symbolic': 's', 'force': 'f', 'no-dereference': 'n', 'verbose': 'v', 'relative': 'r' } }) const [target, link] = options.operands if (!target) return undefined const made = link ? [...context.translate(link.node), space()] : [] return withUnknown([ verb(options.flags.has('s') ? '建符号链接' : '建硬链接'), space(), ...made, word('→'), space(), ...context.translate(target.node), ...notes([...(link ? [] : [[hint('放在当前目录')]]), ...meanings(options, [['f', '已有就替换'], ['r', '按相对路径']])]), ], options) }) const touch = shellCommand('touch', ['touch'], (command, context) => { const options = parseOptions(command.arguments, { flags: 'acm', long: { 'no-create': 'c' } }) return withUnknown([verb('更新修改时间'), space(), ...operandsText(options.operands, context), ...notes(options.flags.has('c') ? [[hint('没有的不建')]] : [[hint('没有就建空文件')]])], options) }) const source = shellCommand('source', ['source', '.'], (command, context) => { const [file, ...args] = command.arguments if (!file) return undefined return [verb('载入并执行'), space(), ...context.translate(file.node), ...(args.length > 0 ? [word(',参数'), space(), ...operandsText(args, context)] : [])] }) const read = shellCommand('read', ['read'], (command, context) => { const options = parseOptions(command.arguments, { flags: 'rse', valued: 'ptdna' }) const prompt = options.values.get('p') return withUnknown([ verb('读一行进'), space(), ...operandsText(options.operands, context), ...notes([ ...(prompt ? [[hint('提示 '), ...valueText(prompt, context)]] : []), ...meanings(options, [['r', '不认转义'], ['s', '不回显']]), ]), ], options) }) const sleep = shellCommand('sleep', ['sleep'], (command, context) => { const [time] = command.arguments return time ? [verb('等'), space(), ...context.translate(time.node), space(), word('秒')] : undefined }) const flow = [ shellCommand('continue', ['continue'], () => [verb('跳过这一个')]), shellCommand('break', ['break'], () => [verb('跳出循环')]), shellCommand('true', ['true', ':'], () => [verb('什么也不做')]), shellCommand('false', ['false'], () => [verb('失败')]), ] /** 内置的命令规则:常用的文件、下载、打包、文本处理命令和 shell 自带的几个命令。 */ export const shellCommandRules: CommandRule[] = [ rm, mkdir, curl, tar, unzip, find, cp, mv, echo, printf, exit, returns, cd, pwd, basename, dirname, readlink, shasum, ...sums, cut, du, cat, grep, sed, chmod, ln, touch, source, read, sleep, ...flow, ]