blob: c2b75819978415beaef91796d86a853a7d592268 [file] [log] [blame]
Yang Guo4fd355c2019-09-19 10:59:03 +02001'use strict';
2
Yang Guo4fd355c2019-09-19 10:59:03 +02003const flatten = require('flat');
Peter Marshall4e161df2020-11-10 13:29:38 +01004const camelcase = require('camelcase');
5const decamelize = require('decamelize');
6const isPlainObj = require('is-plain-obj');
Yang Guo4fd355c2019-09-19 10:59:03 +02007
8function isAlias(key, alias) {
Peter Marshall4e161df2020-11-10 13:29:38 +01009 // TODO Switch to Object.values one Node.js 6 is dropped
10 return Object.keys(alias).some((id) => [].concat(alias[id]).indexOf(key) !== -1);
Yang Guo4fd355c2019-09-19 10:59:03 +020011}
12
13function hasDefaultValue(key, value, defaults) {
14 return value === defaults[key];
15}
16
17function isCamelCased(key, argv) {
Peter Marshall4e161df2020-11-10 13:29:38 +010018 return /[A-Z]/.test(key) && camelcase(key) === key && // Is it camel case?
19 argv[decamelize(key, '-')] != null; // Is the standard version defined?
Yang Guo4fd355c2019-09-19 10:59:03 +020020}
21
22function keyToFlag(key) {
23 return key.length === 1 ? `-${key}` : `--${key}`;
24}
25
Peter Marshall4e161df2020-11-10 13:29:38 +010026function parseCommand(cmd) {
27 const extraSpacesStrippedCommand = cmd.replace(/\s{2,}/g, ' ');
28 const splitCommand = extraSpacesStrippedCommand.split(/\s+(?![^[]*]|[^<]*>)/);
29 const bregex = /\.*[\][<>]/g;
30 const firstCommand = splitCommand.shift();
31
32 if (!firstCommand) { throw new Error(`No command found in: ${cmd}`); }
33 const parsedCommand = {
34 cmd: firstCommand.replace(bregex, ''),
35 demanded: [],
36 optional: [],
37 };
38
39 splitCommand.forEach((cmd, i) => {
40 let variadic = false;
41
42 cmd = cmd.replace(/\s/g, '');
43 if (/\.+[\]>]/.test(cmd) && i === splitCommand.length - 1) { variadic = true; }
44 if (/^\[/.test(cmd)) {
45 parsedCommand.optional.push({
46 cmd: cmd.replace(bregex, '').split('|'),
47 variadic,
48 });
49 } else {
50 parsedCommand.demanded.push({
51 cmd: cmd.replace(bregex, '').split('|'),
52 variadic,
53 });
54 }
55 });
56
57 return parsedCommand;
58}
59
Yang Guo4fd355c2019-09-19 10:59:03 +020060function unparseOption(key, value, unparsed) {
61 if (typeof value === 'string') {
62 unparsed.push(keyToFlag(key), value);
63 } else if (value === true) {
64 unparsed.push(keyToFlag(key));
65 } else if (value === false) {
66 unparsed.push(`--no-${key}`);
67 } else if (Array.isArray(value)) {
68 value.forEach((item) => unparseOption(key, item, unparsed));
Peter Marshall4e161df2020-11-10 13:29:38 +010069 } else if (isPlainObj(value)) {
Yang Guo4fd355c2019-09-19 10:59:03 +020070 const flattened = flatten(value, { safe: true });
71
72 for (const flattenedKey in flattened) {
Tim van der Lippe99190c92020-04-07 16:46:32 +010073 if (!isCamelCased(flattenedKey, flattened)) {
74 unparseOption(`${key}.${flattenedKey}`, flattened[flattenedKey], unparsed);
75 }
Yang Guo4fd355c2019-09-19 10:59:03 +020076 }
77 // Fallback case (numbers and other types)
78 } else if (value != null) {
79 unparsed.push(keyToFlag(key), `${value}`);
80 }
81}
82
83function unparsePositional(argv, options, unparsed) {
84 const knownPositional = [];
85
86 // Unparse command if set, collecting all known positional arguments
87 // e.g.: build <first> <second> <rest...>
88 if (options.command) {
89 const { 0: cmd, index } = options.command.match(/[^<[]*/);
Peter Marshall4e161df2020-11-10 13:29:38 +010090 const { demanded, optional } = parseCommand(`foo ${options.command.substr(index + cmd.length)}`);
Yang Guo4fd355c2019-09-19 10:59:03 +020091
92 // Push command (can be a deep command)
93 unparsed.push(...cmd.trim().split(/\s+/));
94
95 // Push positional arguments
96 [...demanded, ...optional].forEach(({ cmd: cmds, variadic }) => {
97 knownPositional.push(...cmds);
98
99 const cmd = cmds[0];
100 const args = (variadic ? argv[cmd] || [] : [argv[cmd]])
101 .filter((arg) => arg != null)
102 .map((arg) => `${arg}`);
103
104 unparsed.push(...args);
105 });
106 }
107
108 // Unparse unkown positional arguments
109 argv._ && unparsed.push(...argv._.slice(knownPositional.length));
110
111 return knownPositional;
112}
113
114function unparseOptions(argv, options, knownPositional, unparsed) {
Peter Marshall4e161df2020-11-10 13:29:38 +0100115 for (const key of Object.keys(argv)) {
116 const value = argv[key];
Yang Guo4fd355c2019-09-19 10:59:03 +0200117
Peter Marshall4e161df2020-11-10 13:29:38 +0100118 if (
119 // Remove positional arguments
120 knownPositional.includes(key) ||
121 // Remove special _, -- and $0
122 ['_', '--', '$0'].includes(key) ||
123 // Remove aliases
124 isAlias(key, options.alias) ||
125 // Remove default values
126 hasDefaultValue(key, value, options.default) ||
127 // Remove camel-cased
128 isCamelCased(key, argv)
129 ) {
130 continue;
131 }
132
133 unparseOption(key, argv[key], unparsed);
Yang Guo4fd355c2019-09-19 10:59:03 +0200134 }
135}
136
137function unparseEndOfOptions(argv, options, unparsed) {
138 // Unparse ending (--) arguments if set
139 argv['--'] && unparsed.push('--', ...argv['--']);
140}
141
142// ------------------------------------------------------------
143
144function unparser(argv, options) {
145 options = Object.assign({
146 alias: {},
147 default: {},
148 command: null,
149 }, options);
150
151 const unparsed = [];
152
153 // Unparse known & unknown positional arguments (foo <first> <second> [rest...])
154 // All known positional will be returned so that they are not added as flags
155 const knownPositional = unparsePositional(argv, options, unparsed);
156
157 // Unparse option arguments (--foo hello --bar hi)
158 unparseOptions(argv, options, knownPositional, unparsed);
159
160 // Unparse "end-of-options" arguments (stuff after " -- ")
161 unparseEndOfOptions(argv, options, unparsed);
162
163 return unparsed;
164}
165
166module.exports = unparser;