UNPKG

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