585 lines
26 KiB
TypeScript
585 lines
26 KiB
TypeScript
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,
|
||
]
|