blob: 3e42d6591e16a0f958716e45e9c3d15d2726da0e [file] [log] [blame]
Yang Guo4fd355c2019-09-19 10:59:03 +02001'use strict';
2
3const yargs = require('yargs/yargs');
4const flatten = require('flat');
Wolfgang Beyercdedf582020-08-25 08:24:51 +00005const { castArray, some, isPlainObject, camelCase, kebabCase, omitBy } = require('lodash');
Yang Guo4fd355c2019-09-19 10:59:03 +02006
7function isAlias(key, alias) {
Wolfgang Beyercdedf582020-08-25 08:24:51 +00008 return some(alias, (aliases) => castArray(aliases).indexOf(key) !== -1);
Yang Guo4fd355c2019-09-19 10:59:03 +02009}
10
11function hasDefaultValue(key, value, defaults) {
12 return value === defaults[key];
13}
14
15function isCamelCased(key, argv) {
Wolfgang Beyercdedf582020-08-25 08:24:51 +000016 return /[A-Z]/.test(key) && camelCase(key) === key && // Is it camel case?
17 argv[kebabCase(key)] != null; // Is the standard version defined?
Yang Guo4fd355c2019-09-19 10:59:03 +020018}
19
20function keyToFlag(key) {
21 return key.length === 1 ? `-${key}` : `--${key}`;
22}
23
24function unparseOption(key, value, unparsed) {
25 if (typeof value === 'string') {
26 unparsed.push(keyToFlag(key), value);
27 } else if (value === true) {
28 unparsed.push(keyToFlag(key));
29 } else if (value === false) {
30 unparsed.push(`--no-${key}`);
31 } else if (Array.isArray(value)) {
32 value.forEach((item) => unparseOption(key, item, unparsed));
Wolfgang Beyercdedf582020-08-25 08:24:51 +000033 } else if (isPlainObject(value)) {
Yang Guo4fd355c2019-09-19 10:59:03 +020034 const flattened = flatten(value, { safe: true });
35
36 for (const flattenedKey in flattened) {
Tim van der Lippe99190c92020-04-07 16:46:32 +010037 if (!isCamelCased(flattenedKey, flattened)) {
38 unparseOption(`${key}.${flattenedKey}`, flattened[flattenedKey], unparsed);
39 }
Yang Guo4fd355c2019-09-19 10:59:03 +020040 }
41 // Fallback case (numbers and other types)
42 } else if (value != null) {
43 unparsed.push(keyToFlag(key), `${value}`);
44 }
45}
46
47function unparsePositional(argv, options, unparsed) {
48 const knownPositional = [];
49
50 // Unparse command if set, collecting all known positional arguments
51 // e.g.: build <first> <second> <rest...>
52 if (options.command) {
53 const { 0: cmd, index } = options.command.match(/[^<[]*/);
54 const { demanded, optional } = yargs()
55 .getCommandInstance()
56 .parseCommand(`foo ${options.command.substr(index + cmd.length)}`);
57
58 // Push command (can be a deep command)
59 unparsed.push(...cmd.trim().split(/\s+/));
60
61 // Push positional arguments
62 [...demanded, ...optional].forEach(({ cmd: cmds, variadic }) => {
63 knownPositional.push(...cmds);
64
65 const cmd = cmds[0];
66 const args = (variadic ? argv[cmd] || [] : [argv[cmd]])
67 .filter((arg) => arg != null)
68 .map((arg) => `${arg}`);
69
70 unparsed.push(...args);
71 });
72 }
73
74 // Unparse unkown positional arguments
75 argv._ && unparsed.push(...argv._.slice(knownPositional.length));
76
77 return knownPositional;
78}
79
80function unparseOptions(argv, options, knownPositional, unparsed) {
Wolfgang Beyercdedf582020-08-25 08:24:51 +000081 const optionsArgv = omitBy(argv, (value, key) =>
82 // Remove positional arguments
83 knownPositional.includes(key) ||
84 // Remove special _, -- and $0
85 ['_', '--', '$0'].includes(key) ||
86 // Remove aliases
87 isAlias(key, options.alias) ||
88 // Remove default values
89 hasDefaultValue(key, value, options.default) ||
90 // Remove camel-cased
91 isCamelCased(key, argv));
Yang Guo4fd355c2019-09-19 10:59:03 +020092
Wolfgang Beyercdedf582020-08-25 08:24:51 +000093 for (const key in optionsArgv) {
94 unparseOption(key, optionsArgv[key], unparsed);
Yang Guo4fd355c2019-09-19 10:59:03 +020095 }
96}
97
98function unparseEndOfOptions(argv, options, unparsed) {
99 // Unparse ending (--) arguments if set
100 argv['--'] && unparsed.push('--', ...argv['--']);
101}
102
103// ------------------------------------------------------------
104
105function unparser(argv, options) {
106 options = Object.assign({
107 alias: {},
108 default: {},
109 command: null,
110 }, options);
111
112 const unparsed = [];
113
114 // Unparse known & unknown positional arguments (foo <first> <second> [rest...])
115 // All known positional will be returned so that they are not added as flags
116 const knownPositional = unparsePositional(argv, options, unparsed);
117
118 // Unparse option arguments (--foo hello --bar hi)
119 unparseOptions(argv, options, knownPositional, unparsed);
120
121 // Unparse "end-of-options" arguments (stuff after " -- ")
122 unparseEndOfOptions(argv, options, unparsed);
123
124 return unparsed;
125}
126
127module.exports = unparser;