UNPKG

4.6 kBJavaScriptView Raw
1const { InvalidArgumentError } = require('./error.js');
2
3// @ts-check
4
5class Option {
6 /**
7 * Initialize a new `Option` with the given `flags` and `description`.
8 *
9 * @param {string} flags
10 * @param {string} [description]
11 */
12
13 constructor(flags, description) {
14 this.flags = flags;
15 this.description = description || '';
16
17 this.required = flags.includes('<'); // A value must be supplied when the option is specified.
18 this.optional = flags.includes('['); // A value is optional when the option is specified.
19 // variadic test ignores <value,...> et al which might be used to describe custom splitting of single argument
20 this.variadic = /\w\.\.\.[>\]]$/.test(flags); // The option can take multiple values.
21 this.mandatory = false; // The option must have a value after parsing, which usually means it must be specified on command line.
22 const optionFlags = splitOptionFlags(flags);
23 this.short = optionFlags.shortFlag;
24 this.long = optionFlags.longFlag;
25 this.negate = false;
26 if (this.long) {
27 this.negate = this.long.startsWith('--no-');
28 }
29 this.defaultValue = undefined;
30 this.defaultValueDescription = undefined;
31 this.parseArg = undefined;
32 this.hidden = false;
33 this.argChoices = undefined;
34 }
35
36 /**
37 * Set the default value, and optionally supply the description to be displayed in the help.
38 *
39 * @param {any} value
40 * @param {string} [description]
41 * @return {Option}
42 */
43
44 default(value, description) {
45 this.defaultValue = value;
46 this.defaultValueDescription = description;
47 return this;
48 };
49
50 /**
51 * Set the custom handler for processing CLI option arguments into option values.
52 *
53 * @param {Function} [fn]
54 * @return {Option}
55 */
56
57 argParser(fn) {
58 this.parseArg = fn;
59 return this;
60 };
61
62 /**
63 * Whether the option is mandatory and must have a value after parsing.
64 *
65 * @param {boolean} [mandatory=true]
66 * @return {Option}
67 */
68
69 makeOptionMandatory(mandatory = true) {
70 this.mandatory = !!mandatory;
71 return this;
72 };
73
74 /**
75 * Hide option in help.
76 *
77 * @param {boolean} [hide=true]
78 * @return {Option}
79 */
80
81 hideHelp(hide = true) {
82 this.hidden = !!hide;
83 return this;
84 };
85
86 /**
87 * @api private
88 */
89
90 _concatValue(value, previous) {
91 if (previous === this.defaultValue || !Array.isArray(previous)) {
92 return [value];
93 }
94
95 return previous.concat(value);
96 }
97
98 /**
99 * Only allow option value to be one of choices.
100 *
101 * @param {string[]} values
102 * @return {Option}
103 */
104
105 choices(values) {
106 this.argChoices = values;
107 this.parseArg = (arg, previous) => {
108 if (!values.includes(arg)) {
109 throw new InvalidArgumentError(`Allowed choices are ${values.join(', ')}.`);
110 }
111 if (this.variadic) {
112 return this._concatValue(arg, previous);
113 }
114 return arg;
115 };
116 return this;
117 };
118
119 /**
120 * Return option name.
121 *
122 * @return {string}
123 */
124
125 name() {
126 if (this.long) {
127 return this.long.replace(/^--/, '');
128 }
129 return this.short.replace(/^-/, '');
130 };
131
132 /**
133 * Return option name, in a camelcase format that can be used
134 * as a object attribute key.
135 *
136 * @return {string}
137 * @api private
138 */
139
140 attributeName() {
141 return camelcase(this.name().replace(/^no-/, ''));
142 };
143
144 /**
145 * Check if `arg` matches the short or long flag.
146 *
147 * @param {string} arg
148 * @return {boolean}
149 * @api private
150 */
151
152 is(arg) {
153 return this.short === arg || this.long === arg;
154 };
155}
156
157/**
158 * Convert string from kebab-case to camelCase.
159 *
160 * @param {string} str
161 * @return {string}
162 * @api private
163 */
164
165function camelcase(str) {
166 return str.split('-').reduce((str, word) => {
167 return str + word[0].toUpperCase() + word.slice(1);
168 });
169}
170
171/**
172 * Split the short and long flag out of something like '-m,--mixed <value>'
173 *
174 * @api private
175 */
176
177function splitOptionFlags(flags) {
178 let shortFlag;
179 let longFlag;
180 // Use original very loose parsing to maintain backwards compatibility for now,
181 // which allowed for example unintended `-sw, --short-word` [sic].
182 const flagParts = flags.split(/[ |,]+/);
183 if (flagParts.length > 1 && !/^[[<]/.test(flagParts[1])) shortFlag = flagParts.shift();
184 longFlag = flagParts.shift();
185 // Add support for lone short flag without significantly changing parsing!
186 if (!shortFlag && /^-[^-]$/.test(longFlag)) {
187 shortFlag = longFlag;
188 longFlag = undefined;
189 }
190 return { shortFlag, longFlag };
191}
192
193exports.Option = Option;
194exports.splitOptionFlags = splitOptionFlags;