cli.mjs 7.2 KB

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