forked from eemeli/yaml
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.ts
More file actions
215 lines (199 loc) · 6.39 KB
/
cli.ts
File metadata and controls
215 lines (199 loc) · 6.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import { resolve } from 'node:path'
import { parseArgs } from 'node:util'
import type { Token } from './parse/cst.ts'
import { prettyToken } from './parse/cst.ts'
import { Lexer } from './parse/lexer.ts'
import { Parser } from './parse/parser.ts'
import { Composer } from './compose/composer.ts'
import { LineCounter } from './parse/line-counter.ts'
import type { Document } from './doc/Document.ts'
import { prettifyError } from './errors.ts'
import type { visitor } from './visit.ts'
import { visit } from './visit.ts'
export const help = `\
yaml: A command-line YAML processor and inspector
Reads stdin and writes output to stdout and errors & warnings to stderr.
Usage:
yaml Process a YAML stream, outputting it as YAML
yaml cst Parse the CST of a YAML stream
yaml lex Parse the lexical tokens of a YAML stream
yaml valid Validate a YAML stream, returning 0 on success
Options:
--help, -h Show this message.
--json, -j Output JSON.
--indent 2 Output pretty-printed data, indented by the given number of spaces.
--merge, -m Enable support for "<<" merge keys.
Additional options for bare "yaml" command:
--doc, -d Output pretty-printed JS Document objects.
--single, -1 Require the input to consist of a single YAML document.
--strict, -s Stop on errors.
--visit, -v Apply a visitor to each document (requires a path to import)
--yaml 1.1 Set the YAML version. (default: 1.2)`
export class UserError extends Error {
static ARGS = 2
static SINGLE = 3
code: number
constructor(code: number, message: string) {
super(`Error: ${message}`)
this.code = code
}
}
export async function cli(
stdin: NodeJS.ReadableStream,
done: (error?: Error) => void,
argv?: string[]
) {
let args
try {
args = parseArgs({
args: argv,
allowPositionals: true,
options: {
doc: { type: 'boolean', short: 'd' },
help: { type: 'boolean', short: 'h' },
indent: { type: 'string', short: 'i' },
merge: { type: 'boolean', short: 'm' },
json: { type: 'boolean', short: 'j' },
single: { type: 'boolean', short: '1' },
strict: { type: 'boolean', short: 's' },
visit: { type: 'string', short: 'v' },
yaml: { type: 'string', default: '1.2' }
}
})
} catch (error) {
return done(new UserError(UserError.ARGS, (error as Error).message))
}
const {
positionals: [mode],
values: opt
} = args
let indent = Number(opt.indent)
stdin.setEncoding('utf-8')
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
switch (opt.help || mode) {
/* istanbul ignore next */
case true: // --help
console.log(help)
break
case 'lex': {
const lexer = new Lexer()
const data: string[] = []
const add = (tok: string) => {
if (opt.json) data.push(tok)
else console.log(prettyToken(tok))
}
stdin.on('data', (chunk: string) => {
for (const tok of lexer.lex(chunk, true)) add(tok)
})
stdin.on('end', () => {
for (const tok of lexer.lex('', false)) add(tok)
if (opt.json) console.log(JSON.stringify(data, null, indent))
done()
})
break
}
case 'cst': {
const parser = new Parser()
const data: Token[] = []
const add = (tok: Token) => {
if (opt.json) data.push(tok)
else console.dir(tok, { depth: null })
}
stdin.on('data', (chunk: string) => {
for (const tok of parser.parse(chunk, true)) add(tok)
})
stdin.on('end', () => {
for (const tok of parser.parse('', false)) add(tok)
if (opt.json) console.log(JSON.stringify(data, null, indent))
done()
})
break
}
case undefined:
case 'valid': {
const lineCounter = new LineCounter()
const parser = new Parser(lineCounter.addNewLine)
// @ts-expect-error Version is validated at runtime
const composer = new Composer({ version: opt.yaml, merge: opt.merge })
const visitor: visitor | null = opt.visit
? (await import(resolve(opt.visit))).default
: null
let source = ''
let hasDoc = false
let reqDocEnd = false
const data: Document[] = []
const add = (doc: Document) => {
if (hasDoc && opt.single) {
return done(
new UserError(
UserError.SINGLE,
'Input stream contains multiple documents'
)
)
}
for (const error of doc.errors) {
prettifyError(source, lineCounter)(error)
if (opt.strict || mode === 'valid') return done(error)
console.error(error)
}
for (const warning of doc.warnings) {
prettifyError(source, lineCounter)(warning)
console.error(warning)
}
if (visitor) visit(doc, visitor)
if (mode === 'valid') doc.toJS()
else if (opt.json) data.push(doc)
else if (opt.doc) {
Object.defineProperties(doc, {
options: { enumerable: false },
schema: { enumerable: false }
})
console.dir(doc, { depth: null })
} else {
if (reqDocEnd) console.log('...')
try {
indent ||= 2
const str = doc.toString({ indent })
console.log(str.endsWith('\n') ? str.slice(0, -1) : str)
} catch (error) {
done(error as Error)
}
}
hasDoc = true
reqDocEnd = !doc.directives?.docEnd
}
stdin.on('data', (chunk: string) => {
source += chunk
for (const tok of parser.parse(chunk, true)) {
for (const doc of composer.next(tok)) add(doc)
}
})
stdin.on('end', () => {
for (const tok of parser.parse('', false)) {
for (const doc of composer.next(tok)) add(doc)
}
for (const doc of composer.end(false)) add(doc)
if (opt.single && !hasDoc) {
return done(
new UserError(
UserError.SINGLE,
'Input stream contained no documents'
)
)
}
if (mode !== 'valid' && opt.json) {
console.log(JSON.stringify(opt.single ? data[0] : data, null, indent))
}
done()
})
break
}
default:
done(
new UserError(
UserError.ARGS,
`Unknown command: ${JSON.stringify(mode)}`
)
)
}
}