UNPKG

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