Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/git/src/util.ts
3316 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Event, Disposable, EventEmitter, SourceControlHistoryItemRef, l10n, workspace, Uri, DiagnosticSeverity, env } from 'vscode';
7
import { dirname, sep, relative } from 'path';
8
import { Readable } from 'stream';
9
import { promises as fs, createReadStream } from 'fs';
10
import byline from 'byline';
11
12
export const isMacintosh = process.platform === 'darwin';
13
export const isWindows = process.platform === 'win32';
14
export const isRemote = env.remoteName !== undefined;
15
export const isLinux = process.platform === 'linux';
16
export const isLinuxSnap = isLinux && !!process.env['SNAP'] && !!process.env['SNAP_REVISION'];
17
18
export function log(...args: any[]): void {
19
console.log.apply(console, ['git:', ...args]);
20
}
21
22
export interface IDisposable {
23
dispose(): void;
24
}
25
26
export function dispose<T extends IDisposable>(disposables: T[]): T[] {
27
disposables.forEach(d => d.dispose());
28
return [];
29
}
30
31
export function toDisposable(dispose: () => void): IDisposable {
32
return { dispose };
33
}
34
35
export function combinedDisposable(disposables: IDisposable[]): IDisposable {
36
return toDisposable(() => dispose(disposables));
37
}
38
39
export const EmptyDisposable = toDisposable(() => null);
40
41
export function fireEvent<T>(event: Event<T>): Event<T> {
42
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => event(_ => (listener as any).call(thisArgs), null, disposables);
43
}
44
45
export function mapEvent<I, O>(event: Event<I>, map: (i: I) => O): Event<O> {
46
return (listener: (e: O) => any, thisArgs?: any, disposables?: Disposable[]) => event(i => listener.call(thisArgs, map(i)), null, disposables);
47
}
48
49
export function filterEvent<T>(event: Event<T>, filter: (e: T) => boolean): Event<T> {
50
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables);
51
}
52
53
export function runAndSubscribeEvent<T>(event: Event<T>, handler: (e: T) => any, initial: T): IDisposable;
54
export function runAndSubscribeEvent<T>(event: Event<T>, handler: (e: T | undefined) => any): IDisposable;
55
export function runAndSubscribeEvent<T>(event: Event<T>, handler: (e: T | undefined) => any, initial?: T): IDisposable {
56
handler(initial);
57
return event(e => handler(e));
58
}
59
60
export function anyEvent<T>(...events: Event<T>[]): Event<T> {
61
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
62
const result = combinedDisposable(events.map(event => event(i => listener.call(thisArgs, i))));
63
64
disposables?.push(result);
65
66
return result;
67
};
68
}
69
70
export function done<T>(promise: Promise<T>): Promise<void> {
71
return promise.then<void>(() => undefined);
72
}
73
74
export function onceEvent<T>(event: Event<T>): Event<T> {
75
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
76
const result = event(e => {
77
result.dispose();
78
return listener.call(thisArgs, e);
79
}, null, disposables);
80
81
return result;
82
};
83
}
84
85
export function debounceEvent<T>(event: Event<T>, delay: number): Event<T> {
86
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
87
let timer: NodeJS.Timeout;
88
return event(e => {
89
clearTimeout(timer);
90
timer = setTimeout(() => listener.call(thisArgs, e), delay);
91
}, null, disposables);
92
};
93
}
94
95
export function eventToPromise<T>(event: Event<T>): Promise<T> {
96
return new Promise<T>(c => onceEvent(event)(c));
97
}
98
99
export function once(fn: (...args: any[]) => any): (...args: any[]) => any {
100
const didRun = false;
101
102
return (...args) => {
103
if (didRun) {
104
return;
105
}
106
107
return fn(...args);
108
};
109
}
110
111
export function assign<T>(destination: T, ...sources: any[]): T {
112
for (const source of sources) {
113
Object.keys(source).forEach(key => (destination as any)[key] = source[key]);
114
}
115
116
return destination;
117
}
118
119
export function uniqBy<T>(arr: T[], fn: (el: T) => string): T[] {
120
const seen = Object.create(null);
121
122
return arr.filter(el => {
123
const key = fn(el);
124
125
if (seen[key]) {
126
return false;
127
}
128
129
seen[key] = true;
130
return true;
131
});
132
}
133
134
export function groupBy<T>(arr: T[], fn: (el: T) => string): { [key: string]: T[] } {
135
return arr.reduce((result, el) => {
136
const key = fn(el);
137
result[key] = [...(result[key] || []), el];
138
return result;
139
}, Object.create(null));
140
}
141
142
143
export async function mkdirp(path: string, mode?: number): Promise<boolean> {
144
const mkdir = async () => {
145
try {
146
await fs.mkdir(path, mode);
147
} catch (err) {
148
if (err.code === 'EEXIST') {
149
const stat = await fs.stat(path);
150
151
if (stat.isDirectory()) {
152
return;
153
}
154
155
throw new Error(`'${path}' exists and is not a directory.`);
156
}
157
158
throw err;
159
}
160
};
161
162
// is root?
163
if (path === dirname(path)) {
164
return true;
165
}
166
167
try {
168
await mkdir();
169
} catch (err) {
170
if (err.code !== 'ENOENT') {
171
throw err;
172
}
173
174
await mkdirp(dirname(path), mode);
175
await mkdir();
176
}
177
178
return true;
179
}
180
181
export function uniqueFilter<T>(keyFn: (t: T) => string): (t: T) => boolean {
182
const seen: { [key: string]: boolean } = Object.create(null);
183
184
return element => {
185
const key = keyFn(element);
186
187
if (seen[key]) {
188
return false;
189
}
190
191
seen[key] = true;
192
return true;
193
};
194
}
195
196
export function find<T>(array: T[], fn: (t: T) => boolean): T | undefined {
197
let result: T | undefined = undefined;
198
199
array.some(e => {
200
if (fn(e)) {
201
result = e;
202
return true;
203
}
204
205
return false;
206
});
207
208
return result;
209
}
210
211
export async function grep(filename: string, pattern: RegExp): Promise<boolean> {
212
return new Promise<boolean>((c, e) => {
213
const fileStream = createReadStream(filename, { encoding: 'utf8' });
214
const stream = byline(fileStream);
215
stream.on('data', (line: string) => {
216
if (pattern.test(line)) {
217
fileStream.close();
218
c(true);
219
}
220
});
221
222
stream.on('error', e);
223
stream.on('end', () => c(false));
224
});
225
}
226
227
export function readBytes(stream: Readable, bytes: number): Promise<Buffer> {
228
return new Promise<Buffer>((complete, error) => {
229
let done = false;
230
const buffer = Buffer.allocUnsafe(bytes);
231
let bytesRead = 0;
232
233
stream.on('data', (data: Buffer) => {
234
const bytesToRead = Math.min(bytes - bytesRead, data.length);
235
data.copy(buffer, bytesRead, 0, bytesToRead);
236
bytesRead += bytesToRead;
237
238
if (bytesRead === bytes) {
239
(stream as any).destroy(); // Will trigger the close event eventually
240
}
241
});
242
243
stream.on('error', (e: Error) => {
244
if (!done) {
245
done = true;
246
error(e);
247
}
248
});
249
250
stream.on('close', () => {
251
if (!done) {
252
done = true;
253
complete(buffer.slice(0, bytesRead));
254
}
255
});
256
});
257
}
258
259
export const enum Encoding {
260
UTF8 = 'utf8',
261
UTF16be = 'utf16be',
262
UTF16le = 'utf16le'
263
}
264
265
export function detectUnicodeEncoding(buffer: Buffer): Encoding | null {
266
if (buffer.length < 2) {
267
return null;
268
}
269
270
const b0 = buffer.readUInt8(0);
271
const b1 = buffer.readUInt8(1);
272
273
if (b0 === 0xFE && b1 === 0xFF) {
274
return Encoding.UTF16be;
275
}
276
277
if (b0 === 0xFF && b1 === 0xFE) {
278
return Encoding.UTF16le;
279
}
280
281
if (buffer.length < 3) {
282
return null;
283
}
284
285
const b2 = buffer.readUInt8(2);
286
287
if (b0 === 0xEF && b1 === 0xBB && b2 === 0xBF) {
288
return Encoding.UTF8;
289
}
290
291
return null;
292
}
293
294
export function truncate(value: string, maxLength = 20, ellipsis = true): string {
295
return value.length <= maxLength ? value : `${value.substring(0, maxLength)}${ellipsis ? '\u2026' : ''}`;
296
}
297
298
function normalizePath(path: string): string {
299
// Windows & Mac are currently being handled
300
// as case insensitive file systems in VS Code.
301
if (isWindows || isMacintosh) {
302
return path.toLowerCase();
303
}
304
305
return path;
306
}
307
308
export function isDescendant(parent: string, descendant: string): boolean {
309
if (parent === descendant) {
310
return true;
311
}
312
313
if (parent.charAt(parent.length - 1) !== sep) {
314
parent += sep;
315
}
316
317
return normalizePath(descendant).startsWith(normalizePath(parent));
318
}
319
320
export function pathEquals(a: string, b: string): boolean {
321
return normalizePath(a) === normalizePath(b);
322
}
323
324
/**
325
* Given the `repository.root` compute the relative path while trying to preserve
326
* the casing of the resource URI. The `repository.root` segment of the path can
327
* have a casing mismatch if the folder/workspace is being opened with incorrect
328
* casing which is why we attempt to use substring() before relative().
329
*/
330
export function relativePath(from: string, to: string): string {
331
return relativePathWithNoFallback(from, to) ?? relative(from, to);
332
}
333
334
export function relativePathWithNoFallback(from: string, to: string): string | undefined {
335
// There are cases in which the `from` path may contain a trailing separator at
336
// the end (ex: "C:\", "\\server\folder\" (Windows) or "/" (Linux/macOS)) which
337
// is by design as documented in https://github.com/nodejs/node/issues/1765. If
338
// the trailing separator is missing, we add it.
339
if (from.charAt(from.length - 1) !== sep) {
340
from += sep;
341
}
342
343
if (isDescendant(from, to) && from.length < to.length) {
344
return to.substring(from.length);
345
}
346
347
return undefined;
348
}
349
350
export function* splitInChunks(array: string[], maxChunkLength: number): IterableIterator<string[]> {
351
let current: string[] = [];
352
let length = 0;
353
354
for (const value of array) {
355
let newLength = length + value.length;
356
357
if (newLength > maxChunkLength && current.length > 0) {
358
yield current;
359
current = [];
360
newLength = value.length;
361
}
362
363
current.push(value);
364
length = newLength;
365
}
366
367
if (current.length > 0) {
368
yield current;
369
}
370
}
371
372
/**
373
* @returns whether the provided parameter is defined.
374
*/
375
export function isDefined<T>(arg: T | null | undefined): arg is T {
376
return !isUndefinedOrNull(arg);
377
}
378
379
/**
380
* @returns whether the provided parameter is undefined or null.
381
*/
382
export function isUndefinedOrNull(obj: unknown): obj is undefined | null {
383
return (isUndefined(obj) || obj === null);
384
}
385
386
/**
387
* @returns whether the provided parameter is undefined.
388
*/
389
export function isUndefined(obj: unknown): obj is undefined {
390
return (typeof obj === 'undefined');
391
}
392
393
interface ILimitedTaskFactory<T> {
394
factory: () => Promise<T>;
395
c: (value: T | Promise<T>) => void;
396
e: (error?: any) => void;
397
}
398
399
export class Limiter<T> {
400
401
private runningPromises: number;
402
private maxDegreeOfParalellism: number;
403
private outstandingPromises: ILimitedTaskFactory<T>[];
404
405
constructor(maxDegreeOfParalellism: number) {
406
this.maxDegreeOfParalellism = maxDegreeOfParalellism;
407
this.outstandingPromises = [];
408
this.runningPromises = 0;
409
}
410
411
queue(factory: () => Promise<T>): Promise<T> {
412
return new Promise<T>((c, e) => {
413
this.outstandingPromises.push({ factory, c, e });
414
this.consume();
415
});
416
}
417
418
private consume(): void {
419
while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) {
420
const iLimitedTask = this.outstandingPromises.shift()!;
421
this.runningPromises++;
422
423
const promise = iLimitedTask.factory();
424
promise.then(iLimitedTask.c, iLimitedTask.e);
425
promise.then(() => this.consumed(), () => this.consumed());
426
}
427
}
428
429
private consumed(): void {
430
this.runningPromises--;
431
432
if (this.outstandingPromises.length > 0) {
433
this.consume();
434
}
435
}
436
}
437
438
type Completion<T> = { success: true; value: T } | { success: false; err: any };
439
440
export class PromiseSource<T> {
441
442
private _onDidComplete = new EventEmitter<Completion<T>>();
443
444
private _promise: Promise<T> | undefined;
445
get promise(): Promise<T> {
446
if (this._promise) {
447
return this._promise;
448
}
449
450
return eventToPromise(this._onDidComplete.event).then(completion => {
451
if (completion.success) {
452
return completion.value;
453
} else {
454
throw completion.err;
455
}
456
});
457
}
458
459
resolve(value: T): void {
460
if (!this._promise) {
461
this._promise = Promise.resolve(value);
462
this._onDidComplete.fire({ success: true, value });
463
}
464
}
465
466
reject(err: any): void {
467
if (!this._promise) {
468
this._promise = Promise.reject(err);
469
this._onDidComplete.fire({ success: false, err });
470
}
471
}
472
}
473
474
export namespace Versions {
475
declare type VersionComparisonResult = -1 | 0 | 1;
476
477
export interface Version {
478
major: number;
479
minor: number;
480
patch: number;
481
pre?: string;
482
}
483
484
export function compare(v1: string | Version, v2: string | Version): VersionComparisonResult {
485
if (typeof v1 === 'string') {
486
v1 = fromString(v1);
487
}
488
if (typeof v2 === 'string') {
489
v2 = fromString(v2);
490
}
491
492
if (v1.major > v2.major) { return 1; }
493
if (v1.major < v2.major) { return -1; }
494
495
if (v1.minor > v2.minor) { return 1; }
496
if (v1.minor < v2.minor) { return -1; }
497
498
if (v1.patch > v2.patch) { return 1; }
499
if (v1.patch < v2.patch) { return -1; }
500
501
if (v1.pre === undefined && v2.pre !== undefined) { return 1; }
502
if (v1.pre !== undefined && v2.pre === undefined) { return -1; }
503
504
if (v1.pre !== undefined && v2.pre !== undefined) {
505
return v1.pre.localeCompare(v2.pre) as VersionComparisonResult;
506
}
507
508
return 0;
509
}
510
511
export function from(major: string | number, minor: string | number, patch?: string | number, pre?: string): Version {
512
return {
513
major: typeof major === 'string' ? parseInt(major, 10) : major,
514
minor: typeof minor === 'string' ? parseInt(minor, 10) : minor,
515
patch: patch === undefined || patch === null ? 0 : typeof patch === 'string' ? parseInt(patch, 10) : patch,
516
pre: pre,
517
};
518
}
519
520
export function fromString(version: string): Version {
521
const [ver, pre] = version.split('-');
522
const [major, minor, patch] = ver.split('.');
523
return from(major, minor, patch, pre);
524
}
525
}
526
527
export function deltaHistoryItemRefs(before: SourceControlHistoryItemRef[], after: SourceControlHistoryItemRef[]): {
528
added: SourceControlHistoryItemRef[];
529
modified: SourceControlHistoryItemRef[];
530
removed: SourceControlHistoryItemRef[];
531
} {
532
if (before.length === 0) {
533
return { added: after, modified: [], removed: [] };
534
}
535
536
const added: SourceControlHistoryItemRef[] = [];
537
const modified: SourceControlHistoryItemRef[] = [];
538
const removed: SourceControlHistoryItemRef[] = [];
539
540
let beforeIdx = 0;
541
let afterIdx = 0;
542
543
while (true) {
544
if (beforeIdx === before.length) {
545
added.push(...after.slice(afterIdx));
546
break;
547
}
548
if (afterIdx === after.length) {
549
removed.push(...before.slice(beforeIdx));
550
break;
551
}
552
553
const beforeElement = before[beforeIdx];
554
const afterElement = after[afterIdx];
555
556
const result = beforeElement.id.localeCompare(afterElement.id);
557
558
if (result === 0) {
559
if (beforeElement.revision !== afterElement.revision) {
560
// modified
561
modified.push(afterElement);
562
}
563
564
beforeIdx += 1;
565
afterIdx += 1;
566
} else if (result < 0) {
567
// beforeElement is smaller -> before element removed
568
removed.push(beforeElement);
569
570
beforeIdx += 1;
571
} else if (result > 0) {
572
// beforeElement is greater -> after element added
573
added.push(afterElement);
574
575
afterIdx += 1;
576
}
577
}
578
579
return { added, modified, removed };
580
}
581
582
const minute = 60;
583
const hour = minute * 60;
584
const day = hour * 24;
585
const week = day * 7;
586
const month = day * 30;
587
const year = day * 365;
588
589
/**
590
* Create a l10n.td difference of the time between now and the specified date.
591
* @param date The date to generate the difference from.
592
* @param appendAgoLabel Whether to append the " ago" to the end.
593
* @param useFullTimeWords Whether to use full words (eg. seconds) instead of
594
* shortened (eg. secs).
595
* @param disallowNow Whether to disallow the string "now" when the difference
596
* is less than 30 seconds.
597
*/
598
export function fromNow(date: number | Date, appendAgoLabel?: boolean, useFullTimeWords?: boolean, disallowNow?: boolean): string {
599
if (typeof date !== 'number') {
600
date = date.getTime();
601
}
602
603
const seconds = Math.round((new Date().getTime() - date) / 1000);
604
if (seconds < -30) {
605
return l10n.t('in {0}', fromNow(new Date().getTime() + seconds * 1000, false));
606
}
607
608
if (!disallowNow && seconds < 30) {
609
return l10n.t('now');
610
}
611
612
let value: number;
613
if (seconds < minute) {
614
value = seconds;
615
616
if (appendAgoLabel) {
617
if (value === 1) {
618
return useFullTimeWords
619
? l10n.t('{0} second ago', value)
620
: l10n.t('{0} sec ago', value);
621
} else {
622
return useFullTimeWords
623
? l10n.t('{0} seconds ago', value)
624
: l10n.t('{0} secs ago', value);
625
}
626
} else {
627
if (value === 1) {
628
return useFullTimeWords
629
? l10n.t('{0} second', value)
630
: l10n.t('{0} sec', value);
631
} else {
632
return useFullTimeWords
633
? l10n.t('{0} seconds', value)
634
: l10n.t('{0} secs', value);
635
}
636
}
637
}
638
639
if (seconds < hour) {
640
value = Math.floor(seconds / minute);
641
if (appendAgoLabel) {
642
if (value === 1) {
643
return useFullTimeWords
644
? l10n.t('{0} minute ago', value)
645
: l10n.t('{0} min ago', value);
646
} else {
647
return useFullTimeWords
648
? l10n.t('{0} minutes ago', value)
649
: l10n.t('{0} mins ago', value);
650
}
651
} else {
652
if (value === 1) {
653
return useFullTimeWords
654
? l10n.t('{0} minute', value)
655
: l10n.t('{0} min', value);
656
} else {
657
return useFullTimeWords
658
? l10n.t('{0} minutes', value)
659
: l10n.t('{0} mins', value);
660
}
661
}
662
}
663
664
if (seconds < day) {
665
value = Math.floor(seconds / hour);
666
if (appendAgoLabel) {
667
if (value === 1) {
668
return useFullTimeWords
669
? l10n.t('{0} hour ago', value)
670
: l10n.t('{0} hr ago', value);
671
} else {
672
return useFullTimeWords
673
? l10n.t('{0} hours ago', value)
674
: l10n.t('{0} hrs ago', value);
675
}
676
} else {
677
if (value === 1) {
678
return useFullTimeWords
679
? l10n.t('{0} hour', value)
680
: l10n.t('{0} hr', value);
681
} else {
682
return useFullTimeWords
683
? l10n.t('{0} hours', value)
684
: l10n.t('{0} hrs', value);
685
}
686
}
687
}
688
689
if (seconds < week) {
690
value = Math.floor(seconds / day);
691
if (appendAgoLabel) {
692
return value === 1
693
? l10n.t('{0} day ago', value)
694
: l10n.t('{0} days ago', value);
695
} else {
696
return value === 1
697
? l10n.t('{0} day', value)
698
: l10n.t('{0} days', value);
699
}
700
}
701
702
if (seconds < month) {
703
value = Math.floor(seconds / week);
704
if (appendAgoLabel) {
705
if (value === 1) {
706
return useFullTimeWords
707
? l10n.t('{0} week ago', value)
708
: l10n.t('{0} wk ago', value);
709
} else {
710
return useFullTimeWords
711
? l10n.t('{0} weeks ago', value)
712
: l10n.t('{0} wks ago', value);
713
}
714
} else {
715
if (value === 1) {
716
return useFullTimeWords
717
? l10n.t('{0} week', value)
718
: l10n.t('{0} wk', value);
719
} else {
720
return useFullTimeWords
721
? l10n.t('{0} weeks', value)
722
: l10n.t('{0} wks', value);
723
}
724
}
725
}
726
727
if (seconds < year) {
728
value = Math.floor(seconds / month);
729
if (appendAgoLabel) {
730
if (value === 1) {
731
return useFullTimeWords
732
? l10n.t('{0} month ago', value)
733
: l10n.t('{0} mo ago', value);
734
} else {
735
return useFullTimeWords
736
? l10n.t('{0} months ago', value)
737
: l10n.t('{0} mos ago', value);
738
}
739
} else {
740
if (value === 1) {
741
return useFullTimeWords
742
? l10n.t('{0} month', value)
743
: l10n.t('{0} mo', value);
744
} else {
745
return useFullTimeWords
746
? l10n.t('{0} months', value)
747
: l10n.t('{0} mos', value);
748
}
749
}
750
}
751
752
value = Math.floor(seconds / year);
753
if (appendAgoLabel) {
754
if (value === 1) {
755
return useFullTimeWords
756
? l10n.t('{0} year ago', value)
757
: l10n.t('{0} yr ago', value);
758
} else {
759
return useFullTimeWords
760
? l10n.t('{0} years ago', value)
761
: l10n.t('{0} yrs ago', value);
762
}
763
} else {
764
if (value === 1) {
765
return useFullTimeWords
766
? l10n.t('{0} year', value)
767
: l10n.t('{0} yr', value);
768
} else {
769
return useFullTimeWords
770
? l10n.t('{0} years', value)
771
: l10n.t('{0} yrs', value);
772
}
773
}
774
}
775
776
export function getCommitShortHash(scope: Uri, hash: string): string {
777
const config = workspace.getConfiguration('git', scope);
778
const shortHashLength = config.get<number>('commitShortHashLength', 7);
779
return hash.substring(0, shortHashLength);
780
}
781
782
export type DiagnosticSeverityConfig = 'error' | 'warning' | 'information' | 'hint' | 'none';
783
784
export function toDiagnosticSeverity(value: DiagnosticSeverityConfig): DiagnosticSeverity {
785
return value === 'error'
786
? DiagnosticSeverity.Error
787
: value === 'warning'
788
? DiagnosticSeverity.Warning
789
: value === 'information'
790
? DiagnosticSeverity.Information
791
: DiagnosticSeverity.Hint;
792
}
793
794
export function extractFilePathFromArgs(argv: string[], startIndex: number): string {
795
// Argument doesn't start with a quote
796
const firstArg = argv[startIndex];
797
if (!firstArg.match(/^["']/)) {
798
return firstArg.replace(/^["']+|["':]+$/g, '');
799
}
800
801
// If it starts with a quote, we need to find the matching closing
802
// quote which might be in a later argument if the path contains
803
// spaces
804
const quote = firstArg[0];
805
806
// If the first argument ends with the same quote, it's complete
807
if (firstArg.endsWith(quote) && firstArg.length > 1) {
808
return firstArg.slice(1, -1);
809
}
810
811
// Concatenate arguments until we find the closing quote
812
let path = firstArg;
813
for (let i = startIndex + 1; i < argv.length; i++) {
814
path = `${path} ${argv[i]}`;
815
if (argv[i].endsWith(quote)) {
816
// Found the matching quote
817
return path.slice(1, -1);
818
}
819
}
820
821
// If no closing quote was found, remove
822
// leading quote and return the path as-is
823
return path.slice(1);
824
}
825
826