lib/completion.ts

414 LOC · 414 covered · 0 uncovered · 109 ranges · 1119 concepts · 44 introducers · 788 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > import {CommandInstance, isCommandBuilderCallback} from './command.js'; yargs-factory.ts ×124
2 > import {PlatformShim, assertNotStrictEqual} from './typings/common-types.js';
3 > import * as templates from './completion-templates.js';
4 > import {isPromise} from './utils/is-promise.js';
5 > import {parseCommand} from './parse-command.js';
6 > import {UsageInstance} from './usage.js';
7 > import {YargsInstance} from './yargs-factory.js';
8 > import {Arguments, DetailedArguments} from './typings/yargs-parser-types.js';
9 >
10 > // add bash completions to your
11 > // yargs-powered applications.
12 >
13 > type CompletionCallback = (
14 > err: Error | null,
15 > completions: string[] | undefined
16 > ) => void;
17 >
18 > /** Instance of the completion module. */
19 > export interface CompletionInstance {
20 > completionKey: string;
21 > generateCompletionScript($0: string, cmd: string): string;
22 > getCompletion(
23 > args: string[],
24 > done: (err: Error | null, completions: string[] | undefined) => void
25 > ): any;
26 > registerFunction(fn: CompletionFunction): void;
27 > setParsed(parsed: DetailedArguments): void;
28 > }
29 >
30 > export class Completion implements CompletionInstance {
31 > completionKey = 'get-yargs-completions';
32 >
33 > private aliases: DetailedArguments['aliases'] | null = null;
34 > private customCompletionFunction: CompletionFunction | null = null;
35 > private indexAfterLastReset = 0;
36 > private readonly zshShell: boolean;
37 >
38 > constructor(
39 > private readonly yargs: YargsInstance, yargs-factory.ts ×35
40 > private readonly usage: UsageInstance,
41 > private readonly command: CommandInstance,
42 > private readonly shim: PlatformShim
43 > ) {
44 > this.zshShell =
45 > (this.shim.getEnv('SHELL')?.includes('zsh') ||
46 > this.shim.getEnv('ZSH_NAME')?.includes('zsh')) ??
47 > false;
48 > }
50 > private defaultCompletion(
51 > args: string[], completion.ts ×19
52 > argv: Arguments,
53 > current: string,
54 > done: CompletionCallback
55 > ): Arguments | void {
56 > const handlers = this.command.getCommandHandlers();
57 > for (let i = 0, ii = args.length; i < ii; ++i) {
58 > if (handlers[args[i]] && handlers[args[i]].builder) { completion.ts ×4
59 > const builder = handlers[args[i]].builder; completion.ts ×2
60 > if (isCommandBuilderCallback(builder)) {
61 > this.indexAfterLastReset = i + 1; completion.ts ×1
62 > const y = this.yargs.getInternalMethods().reset();
63 > builder(y, true);
64 > return y.argv;
65 > }
69 > const completions: string[] = [];
70 >
71 > this.commandCompletions(completions, args, current);
72 > this.optionCompletions(completions, args, argv, current);
73 > this.choicesFromOptionsCompletions(completions, args, argv, current);
74 > this.choicesFromPositionalsCompletions(completions, args, argv, current);
75 > done(null, completions);
76 > }
78 > // Default completions for commands
79 > private commandCompletions(
80 > completions: string[], completion.ts ×19
81 > args: string[],
82 > current: string
83 > ) {
84 > const parentCommands = this.yargs
85 > .getInternalMethods()
86 > .getContext().commands;
87 > if (
88 > !current.match(/^-/) &&
89 > parentCommands[parentCommands.length - 1] !== current &&
90 > !this.previousArgHasChoices(args) completion.ts ×1
92 > this.usage.getCommands().forEach(usageCommand => { completion.ts ×2
93 > const commandName = parseCommand(usageCommand[0]).cmd; completion.ts ×3
94 > if (args.indexOf(commandName) === -1) {
95 > if (!this.zshShell) {
96 > completions.push(commandName); completion.ts ×1
97 > } else { completion.ts ×3
98 > const desc = usageCommand[1] || ''; completion.ts ×1
99 > completions.push(commandName.replace(/:/g, '\\:') + ':' + desc);
100 > }
102 > }); completion.ts ×2
103 > }
106 > // Default completions for - and -- options
107 > private optionCompletions(
108 > completions: string[], completion.ts ×19
109 > args: string[],
110 > argv: Arguments,
111 > current: string
112 > ) {
113 > if (
114 > (current.match(/^-/) || (current === '' && completions.length === 0)) &&
115 > !this.previousArgHasChoices(args) completion.ts ×1
116 > ) { completion.ts ×19
117 > const options = this.yargs.getOptions(); completion.ts ×2
118 > const positionalKeys =
119 > this.yargs.getGroups()[this.usage.getPositionalGroupName()] || [];
120 >
121 > Object.keys(options.key).forEach(key => {
122 > const negable = completion.ts ×2
123 > !!options.configuration['boolean-negation'] &&
124 > options.boolean.includes(key);
125 > const isPositionalKey = positionalKeys.includes(key);
126 >
127 > // If the key is not positional and its aliases aren't in 'args', add the key to 'completions'
128 > if (
129 > !isPositionalKey &&
130 > !options.hiddenOptions.includes(key) &&
131 > !this.argsContainKey(args, key, negable) completion.ts ×2
132 > ) { completion.ts ×2
133 > this.completeOptionKey( completion.ts ×6
134 > key,
135 > completions,
136 > current,
137 > negable && !!options.default[key]
138 > );
139 > }
140 > }); completion.ts ×2
141 > }
144 > private choicesFromOptionsCompletions(
145 > completions: string[], completion.ts ×19
146 > args: string[],
147 > argv: Arguments,
148 > current: string
149 > ) {
150 > if (this.previousArgHasChoices(args)) {
151 > const choices = this.getPreviousArgChoices(args); completion.ts ×3
152 > if (choices && choices.length > 0) {
153 > completions.push(...choices.map(c => c.replace(/:/g, '\\:')));
154 > }
155 > }
158 > private choicesFromPositionalsCompletions(
159 > completions: string[], completion.ts ×19
160 > args: string[],
161 > argv: Arguments,
162 > current: string
163 > ) {
164 > if (
165 > current === '' &&
166 > completions.length > 0 &&
167 > this.previousArgHasChoices(args) completion.ts ×1
168 > ) { completion.ts ×19
169 > return; completion.ts ×1
170 > }
172 > const positionalKeys =
173 > this.yargs.getGroups()[this.usage.getPositionalGroupName()] || []; completion.ts ×19
174 > const offset = Math.max(
175 > this.indexAfterLastReset,
176 > this.yargs.getInternalMethods().getContext().commands.length +
177 > /* name of the script is first param */ 1
178 > );
179 >
180 > const positionalKey = positionalKeys[argv._.length - offset - 1];
181 > if (!positionalKey) {
182 > return; completion.ts ×1
183 > }
185 > const choices = this.yargs.getOptions().choices[positionalKey] || []; completion.ts ×19
186 > for (const choice of choices) {
187 > if (choice.startsWith(current)) { completion.ts ×1
188 > completions.push(choice.replace(/:/g, '\\:'));
189 > }
190 > }
193 > private getPreviousArgChoices(args: string[]): string[] | void {
194 > if (args.length < 1) return; // no args completion.ts ×19
195 > let previousArg = args[args.length - 1]; completion.ts ×4
196 > let filter = '';
197 > // use second to last argument if the last one is not an option starting with --
198 > if (!previousArg.startsWith('-') && args.length > 1) { completion.ts ×19
199 > filter = previousArg; // use last arg as filter for choices completion.ts ×1
200 > previousArg = args[args.length - 2];
201 > }
202 > if (!previousArg.startsWith('-')) return; // still no valid arg, abort completion.ts ×4
203 > const previousArgKey = previousArg.replace(/^-+/, ''); completion.ts ×4
204 >
205 > const options = this.yargs.getOptions();
206 >
207 > const possibleAliases = [
208 > previousArgKey,
209 > ...(this.yargs.getAliases()[previousArgKey] || []), completion.ts ×19
210 > ];
211 > let choices: string[] | undefined;
212 > // Find choices across all possible aliases
213 > for (const possibleAlias of possibleAliases) {
214 > if ( completion.ts ×4
215 > Object.prototype.hasOwnProperty.call(options.key, possibleAlias) &&
216 > Array.isArray(options.choices[possibleAlias]) completion.ts ×1
217 > ) { completion.ts ×4
218 > choices = options.choices[possibleAlias]; completion.ts ×3
219 > break;
220 > }
222 >
223 > if (choices) {
224 > return choices.filter(choice => !filter || choice.startsWith(filter)); completion.ts ×3
225 > }
228 > private previousArgHasChoices(args: string[]): boolean {
229 > const choices = this.getPreviousArgChoices(args); completion.ts ×19
230 > return choices !== undefined && choices.length > 0;
231 > }
233 > private argsContainKey(
234 > args: string[], completion.ts ×2
235 > key: string,
236 > negable: boolean
237 > ): boolean {
238 > const argsContains = (s: string) =>
239 > args.indexOf((/^[^0-9]$/.test(s) ? '-' : '--') + s) !== -1;
240 > if (argsContains(key)) return true;
241 > if (negable && argsContains(`no-${key}`)) return true;
242 > if (this.aliases) { completion.ts ×6
243 > for (const alias of this.aliases[key]) {
244 > if (argsContains(alias)) return true; completion.ts ×1
245 > }
247 > return false;
248 > }
250 > // Add completion for a single - or -- option
251 > private completeOptionKey(
252 > key: string, completion.ts ×6
253 > completions: string[],
254 > current: string,
255 > negable: boolean
256 > ) {
257 > let keyWithDesc = key;
258 > if (this.zshShell) {
259 > const descs = this.usage.getDescriptions(); completion.ts ×2
260 > const aliasKey = this?.aliases?.[key]?.find(alias => {
261 > const desc = descs[alias]; completion.ts ×1
262 > return typeof desc === 'string' && desc.length > 0;
263 > }); completion.ts ×2
264 > const descFromAlias = aliasKey ? descs[aliasKey] : undefined;
265 > const desc = descs[key] ?? descFromAlias ?? '';
266 > keyWithDesc = `${key.replace(/:/g, '\\:')}:${desc
267 > .replace('__yargsString__:', '')
268 > .replace(/(\r\n|\n|\r)/gm, ' ')}`;
269 > }
271 > const startsByTwoDashes = (s: string) => /^--/.test(s);
272 > const isShortOption = (s: string) => /^[^0-9]$/.test(s);
273 > const dashes =
274 > !startsByTwoDashes(current) && isShortOption(key) ? '-' : '--';
275 >
276 > completions.push(dashes + keyWithDesc);
277 > if (negable) {
278 > completions.push(dashes + 'no-' + keyWithDesc); completion.ts ×1
279 > }
282 > // a custom completion function can be provided
283 > // to completion().
284 > private customCompletion(
285 > args: string[], completion.ts ×4
286 > argv: Arguments,
287 > current: string,
288 > done: CompletionCallback
289 > ) {
290 > assertNotStrictEqual(this.customCompletionFunction, null, this.shim);
291 >
292 > if (isSyncCompletionFunction(this.customCompletionFunction)) {
293 > const result = this.customCompletionFunction(current, argv); completion.ts ×1
294 >
295 > // promise based completion function.
296 > if (isPromise(result)) {
297 > return result completion.ts ×3
298 > .then(list => {
299 > this.shim.process.nextTick(() => { completion.ts ×1
300 > done(null, list);
301 > });
303 > .catch(err => {
304 > this.shim.process.nextTick(() => { completion.ts ×1
305 > done(err, undefined);
306 > });
307 > }); completion.ts ×3
308 > }
309 > // synchronous completion function. completion.ts ×1
310 > return done(null, result);
311 > } else if (isFallbackCompletionFunction(this.customCompletionFunction)) { completion.ts ×1
312 > return (this.customCompletionFunction as FallbackCompletionFunction)( completion.ts ×2
313 > current,
314 > argv,
315 > (onCompleted = done) =>
316 > this.defaultCompletion(args, argv, current, onCompleted),
317 > completions => {
318 > done(null, completions); completion.ts ×1
319 > }
321 > } else { completion.ts ×2
322 > return (this.customCompletionFunction as AsyncCompletionFunction)( completion.ts ×1
323 > current,
324 > argv,
325 > completions => {
326 > done(null, completions);
327 > }
328 > );
329 > }
332 > // get a list of completion commands.
333 > // 'args' is the array of strings from the line to be completed
334 > getCompletion(args: string[], done: CompletionCallback): any {
335 > const current = args.length ? args[args.length - 1] : ''; completion.ts ×1
336 > const argv = this.yargs.parse(args, true);
337 >
338 > const completionFunction = this.customCompletionFunction
339 > ? (argv: Arguments) => this.customCompletion(args, argv, current, done)
340 > : (argv: Arguments) => this.defaultCompletion(args, argv, current, done);
341 >
342 > return isPromise(argv)
343 > ? argv.then(completionFunction)
344 > : completionFunction(argv);
345 > }
347 > // generate the completion script to add to your .bashrc.
348 > generateCompletionScript($0: string, cmd: string): string {
349 > let script = this.zshShell completion.ts ×1
350 > ? templates.completionZshTemplate
351 > : templates.completionShTemplate;
352 > const name = this.shim.path.basename($0);
353 >
354 > // add ./ to applications not yet installed as bin.
355 > if ($0.match(/\.js$/)) $0 = `./${$0}`;
356 >
357 > script = script.replace(/{{app_name}}/g, name);
358 > script = script.replace(/{{completion_command}}/g, cmd);
359 > return script.replace(/{{app_path}}/g, $0);
360 > }
362 > // register a function to perform your own custom
363 > // completions. this function can be either
364 > // synchronous or asynchronous.
365 > registerFunction(fn: CompletionFunction) {
366 > this.customCompletionFunction = fn; completion.ts ×4
367 > }
369 > setParsed(parsed: DetailedArguments) {
370 > this.aliases = parsed.aliases; yargs-factory.ts ×2
371 > }
373 >
374 > // For backwards compatibility
375 > export function completion(
376 > yargs: YargsInstance, yargs-factory.ts ×35
377 > usage: UsageInstance,
378 > command: CommandInstance,
379 > shim: PlatformShim
380 > ): CompletionInstance {
381 > return new Completion(yargs, usage, command, shim);
382 > }
384 > export type CompletionFunction =
385 > SyncCompletionFunction | AsyncCompletionFunction | FallbackCompletionFunction;
386 >
387 > interface SyncCompletionFunction {
388 > (current: string, argv: Arguments): string[] | Promise<string[]>;
389 > }
390 >
391 > interface AsyncCompletionFunction {
392 > (current: string, argv: Arguments, done: (completions: string[]) => any): any;
393 > }
394 >
395 > interface FallbackCompletionFunction {
396 > (
397 > current: string,
398 > argv: Arguments,
399 > completionFilter: (onCompleted?: CompletionCallback) => any,
400 > done: (completions: string[]) => any
401 > ): any;
402 > }
403 >
404 > function isSyncCompletionFunction( completion.ts ×4
405 > completionFunction: CompletionFunction
406 > ): completionFunction is SyncCompletionFunction {
407 > return completionFunction.length < 3;
408 > }
410 > function isFallbackCompletionFunction( completion.ts ×2
411 > completionFunction: CompletionFunction
412 > ): completionFunction is FallbackCompletionFunction {
413 > return completionFunction.length > 3;
414 > }