Files
superwork/vscode/src/codeReader/commands/shellCommands.ts
T

585 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 { 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<string, string> = { 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<string>()
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,
]