1 | import { resolve } from 'node:path';
|
2 | import { parseArgs } from 'node:util';
|
3 | import { prettyToken } from './parse/cst.js';
|
4 | import { Lexer } from './parse/lexer.js';
|
5 | import { Parser } from './parse/parser.js';
|
6 | import { Composer } from './compose/composer.js';
|
7 | import { LineCounter } from './parse/line-counter.js';
|
8 | import { prettifyError } from './errors.js';
|
9 | import { visit } from './visit.js';
|
10 |
|
11 | const help = `\
|
12 | yaml: A command-line YAML processor and inspector
|
13 |
|
14 | Reads stdin and writes output to stdout and errors & warnings to stderr.
|
15 |
|
16 | Usage:
|
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 |
|
22 | Options:
|
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 |
|
27 | Additional 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)`;
|
33 | class UserError extends Error {
|
34 | constructor(code, message) {
|
35 | super(`Error: ${message}`);
|
36 | this.code = code;
|
37 | }
|
38 | }
|
39 | UserError.ARGS = 2;
|
40 | UserError.SINGLE = 3;
|
41 | async 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 |
|
66 | switch (opt.help || mode) {
|
67 |
|
68 | case true:
|
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 |
|
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 |
|
199 | export { UserError, cli, help };
|