@augment-vir/node 31.73.3 → 31.73.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/augments/fs/grep.d.ts +2 -2
- package/dist/augments/fs/grep.js +107 -559
- package/dist/augments/fs/read-dir.js +3 -9
- package/dist/augments/path/contains.d.ts +7 -11
- package/dist/augments/path/contains.js +1 -1
- package/dist/augments/path/resolve-import.d.ts +1 -4
- package/dist/augments/path/resolve-import.js +3 -7
- package/dist/augments/terminal/run-cli-script.d.ts +8 -5
- package/dist/augments/terminal/run-cli-script.js +8 -1
- package/dist/augments/terminal/shell.d.ts +11 -20
- package/dist/augments/terminal/shell.js +4 -10
- package/package.json +6 -6
- package/src/augments/fs/grep.ts +135 -810
- package/src/augments/fs/read-dir.ts +3 -12
- package/src/augments/path/contains.ts +6 -10
- package/src/augments/path/resolve-import.ts +10 -14
- package/src/augments/terminal/run-cli-script.ts +10 -5
- package/src/augments/terminal/shell.ts +27 -47
package/src/augments/fs/grep.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import {assert, check} from '@augment-vir/assert';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
arrayToObject,
|
|
4
|
+
getOrSet,
|
|
5
5
|
log,
|
|
6
|
-
|
|
6
|
+
safeMatch,
|
|
7
7
|
type PartialWithUndefined,
|
|
8
8
|
type SelectFrom,
|
|
9
9
|
} from '@augment-vir/common';
|
|
10
|
-
import {
|
|
11
|
-
import {lstat, readdir, stat} from 'node:fs/promises';
|
|
12
|
-
import {isAbsolute, join, resolve} from 'node:path';
|
|
10
|
+
import {join} from 'node:path';
|
|
13
11
|
import {type IsEqual, type RequireExactlyOne} from 'type-fest';
|
|
14
12
|
import {isOperatingSystem, OperatingSystem} from '../os/operating-system.js';
|
|
13
|
+
import {runShellCommand} from '../terminal/shell.js';
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
16
|
* Optional options for {@link grep}.
|
|
@@ -21,7 +20,6 @@ import {isOperatingSystem, OperatingSystem} from '../os/operating-system.js';
|
|
|
21
20
|
* @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
|
|
22
21
|
*/
|
|
23
22
|
export type GrepOptions<CountOnly extends boolean = false> = PartialWithUndefined<{
|
|
24
|
-
/* node:coverage ignore next */
|
|
25
23
|
patternSyntax: RequireExactlyOne<{
|
|
26
24
|
/**
|
|
27
25
|
* -E, --extended-regexp: Interpret PATTERNS as extended regular expressions (EREs, see
|
|
@@ -177,14 +175,8 @@ export type GrepSearchPattern = RequireExactlyOne<{
|
|
|
177
175
|
patterns: string[];
|
|
178
176
|
}>;
|
|
179
177
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
function shellQuote(input: string) {
|
|
183
|
-
return [
|
|
184
|
-
"'",
|
|
185
|
-
input.replaceAll("'", String.raw`'\''`),
|
|
186
|
-
"'",
|
|
187
|
-
].join('');
|
|
178
|
+
function escape(input: string) {
|
|
179
|
+
return input.replaceAll('"', String.raw`\"`).replaceAll('\n', '');
|
|
188
180
|
}
|
|
189
181
|
|
|
190
182
|
function recursiveFlag({
|
|
@@ -206,721 +198,9 @@ function recursiveFlag({
|
|
|
206
198
|
return isOperatingSystem(OperatingSystem.Mac) ? '-RS' : '-R';
|
|
207
199
|
}
|
|
208
200
|
|
|
209
|
-
function isValidMaxCount(maxCount: unknown) {
|
|
210
|
-
return (
|
|
211
|
-
maxCount == undefined ||
|
|
212
|
-
(check.isNumber(maxCount) && Number.isInteger(maxCount) && maxCount >= -1)
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function isOptionalBoolean(input: unknown) {
|
|
217
|
-
return input == undefined || check.isBoolean(input);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function isValidTrueOnlyOptionGroup({
|
|
221
|
-
input,
|
|
222
|
-
values,
|
|
223
|
-
}: Readonly<{
|
|
224
|
-
input: unknown;
|
|
225
|
-
values: ReadonlyArray<unknown>;
|
|
226
|
-
}>) {
|
|
227
|
-
return (
|
|
228
|
-
input == undefined ||
|
|
229
|
-
(check.isObject(input) &&
|
|
230
|
-
values.every((value) => value == undefined || value === true) &&
|
|
231
|
-
values.filter((value) => value === true).length === 1)
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function isValidPatternSyntax(input: unknown) {
|
|
236
|
-
return isValidTrueOnlyOptionGroup({
|
|
237
|
-
input,
|
|
238
|
-
values: check.isObject(input)
|
|
239
|
-
? [
|
|
240
|
-
input.basicRegExp,
|
|
241
|
-
input.extendedRegExp,
|
|
242
|
-
input.fixedStrings,
|
|
243
|
-
]
|
|
244
|
-
: [],
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function isValidMatchType(input: unknown) {
|
|
249
|
-
return isValidTrueOnlyOptionGroup({
|
|
250
|
-
input,
|
|
251
|
-
values: check.isObject(input)
|
|
252
|
-
? [
|
|
253
|
-
input.lineRegExp,
|
|
254
|
-
input.wordRegExp,
|
|
255
|
-
]
|
|
256
|
-
: [],
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function isValidOutput(input: unknown) {
|
|
261
|
-
return isValidTrueOnlyOptionGroup({
|
|
262
|
-
input,
|
|
263
|
-
values: check.isObject(input)
|
|
264
|
-
? [
|
|
265
|
-
input.countOnly,
|
|
266
|
-
input.filesOnly,
|
|
267
|
-
]
|
|
268
|
-
: [],
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
type GrepCommandOutput = {
|
|
273
|
-
exitCode: number | undefined;
|
|
274
|
-
stdout: string;
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
function didGrepFail({exitCode}: Readonly<GrepCommandOutput>) {
|
|
278
|
-
return exitCode == undefined || exitCode > 1;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function spawnGrepProcess({
|
|
282
|
-
args,
|
|
283
|
-
cwd,
|
|
284
|
-
}: Readonly<{
|
|
285
|
-
args: string[];
|
|
286
|
-
cwd: string | undefined;
|
|
287
|
-
}>) {
|
|
288
|
-
try {
|
|
289
|
-
return spawn(grepBinPath, args, {
|
|
290
|
-
cwd,
|
|
291
|
-
stdio: [
|
|
292
|
-
'ignore',
|
|
293
|
-
'pipe',
|
|
294
|
-
'pipe',
|
|
295
|
-
],
|
|
296
|
-
});
|
|
297
|
-
/* node:coverage ignore next 3 */
|
|
298
|
-
} catch {
|
|
299
|
-
return undefined;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
async function runGrepCommand({
|
|
304
|
-
args,
|
|
305
|
-
cwd,
|
|
306
|
-
}: Readonly<{
|
|
307
|
-
args: string[];
|
|
308
|
-
cwd: string | undefined;
|
|
309
|
-
}>): Promise<GrepCommandOutput> {
|
|
310
|
-
return new Promise<GrepCommandOutput>((resolveOutput) => {
|
|
311
|
-
const stdoutChunks: Buffer[] = [];
|
|
312
|
-
const grepProcess = spawnGrepProcess({
|
|
313
|
-
args,
|
|
314
|
-
cwd,
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
if (!grepProcess) {
|
|
318
|
-
resolveOutput({
|
|
319
|
-
exitCode: undefined,
|
|
320
|
-
stdout: '',
|
|
321
|
-
});
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
assert.isDefined(grepProcess.stdout, 'stdout emitter was not created for grep.');
|
|
326
|
-
assert.isDefined(grepProcess.stderr, 'stderr emitter was not created for grep.');
|
|
327
|
-
|
|
328
|
-
grepProcess.stdout.on('data', (chunk) => {
|
|
329
|
-
stdoutChunks.push(Buffer.from(chunk));
|
|
330
|
-
});
|
|
331
|
-
grepProcess.stderr.on('data', () => {});
|
|
332
|
-
/* node:coverage ignore next 5 */
|
|
333
|
-
grepProcess.on('error', () => {
|
|
334
|
-
resolveOutput({
|
|
335
|
-
exitCode: undefined,
|
|
336
|
-
stdout: Buffer.concat(stdoutChunks).toString(),
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
grepProcess.on('close', (rawExitCode) => {
|
|
340
|
-
resolveOutput({
|
|
341
|
-
/* node:coverage ignore next */
|
|
342
|
-
exitCode: rawExitCode ?? undefined,
|
|
343
|
-
stdout: Buffer.concat(stdoutChunks).toString(),
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function redactGrepArgForLogging({
|
|
350
|
-
arg,
|
|
351
|
-
index,
|
|
352
|
-
operandDelimiterIndex,
|
|
353
|
-
previousArg,
|
|
354
|
-
}: Readonly<{
|
|
355
|
-
arg: string;
|
|
356
|
-
index: number;
|
|
357
|
-
operandDelimiterIndex: number;
|
|
358
|
-
previousArg: string | undefined;
|
|
359
|
-
}>) {
|
|
360
|
-
if (previousArg === '-e') {
|
|
361
|
-
return '<pattern>';
|
|
362
|
-
} else if (operandDelimiterIndex >= 0 && index > operandDelimiterIndex) {
|
|
363
|
-
return '<path>';
|
|
364
|
-
} else if (arg.startsWith('--exclude-dir=')) {
|
|
365
|
-
return '--exclude-dir=<glob>';
|
|
366
|
-
} else if (arg.startsWith('--exclude=')) {
|
|
367
|
-
return '--exclude=<glob>';
|
|
368
|
-
} else if (arg.startsWith('--include=')) {
|
|
369
|
-
return '--include=<glob>';
|
|
370
|
-
} else if (arg.startsWith('--max-count=')) {
|
|
371
|
-
return '--max-count=<count>';
|
|
372
|
-
} else {
|
|
373
|
-
return arg;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function formatGrepCommand(args: ReadonlyArray<string>) {
|
|
378
|
-
const operandDelimiterIndex = args.indexOf('--');
|
|
379
|
-
|
|
380
|
-
return [
|
|
381
|
-
'grep',
|
|
382
|
-
...args.map((arg, index) =>
|
|
383
|
-
shellQuote(
|
|
384
|
-
redactGrepArgForLogging({
|
|
385
|
-
arg,
|
|
386
|
-
index,
|
|
387
|
-
operandDelimiterIndex,
|
|
388
|
-
previousArg: args[index - 1],
|
|
389
|
-
}),
|
|
390
|
-
),
|
|
391
|
-
),
|
|
392
|
-
].join(' ');
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function replaceGrepCountOutputArg({
|
|
396
|
-
args,
|
|
397
|
-
replacement,
|
|
398
|
-
}: Readonly<{
|
|
399
|
-
args: ReadonlyArray<string>;
|
|
400
|
-
replacement: string;
|
|
401
|
-
}>) {
|
|
402
|
-
const countArgIndex = args.indexOf('--count');
|
|
403
|
-
|
|
404
|
-
/* node:coverage ignore next */
|
|
405
|
-
return countArgIndex < 0 ? [...args] : args.toSpliced(countArgIndex, 1, replacement);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function replaceGrepSearchOperands({
|
|
409
|
-
args,
|
|
410
|
-
searchParts,
|
|
411
|
-
}: Readonly<{
|
|
412
|
-
args: ReadonlyArray<string>;
|
|
413
|
-
searchParts: ReadonlyArray<string>;
|
|
414
|
-
}>) {
|
|
415
|
-
const operandDelimiterIndex = args.indexOf('--');
|
|
416
|
-
|
|
417
|
-
return [
|
|
418
|
-
...args.slice(0, operandDelimiterIndex + 1),
|
|
419
|
-
...searchParts,
|
|
420
|
-
];
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function extractStringArray(input: unknown) {
|
|
424
|
-
return check.isArray(input) ? input.filter(check.isString).filter(check.isTruthy) : [];
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function extractOptionalStringArray(input: unknown) {
|
|
428
|
-
if (input == undefined) {
|
|
429
|
-
return [];
|
|
430
|
-
} else if (!check.isArray(input) || !input.every(check.isString)) {
|
|
431
|
-
return undefined;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
return input.filter(check.isTruthy);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function extractString(input: unknown) {
|
|
438
|
-
return check.isString(input) && input ? input : undefined;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function extractGrepOptionArrays({
|
|
442
|
-
excludeDirs,
|
|
443
|
-
excludePatterns,
|
|
444
|
-
includeFiles,
|
|
445
|
-
}: Readonly<
|
|
446
|
-
PartialWithUndefined<{
|
|
447
|
-
excludeDirs: unknown;
|
|
448
|
-
excludePatterns: unknown;
|
|
449
|
-
includeFiles: unknown;
|
|
450
|
-
}>
|
|
451
|
-
>) {
|
|
452
|
-
const extractedExcludeDirs = extractOptionalStringArray(excludeDirs);
|
|
453
|
-
const extractedExcludePatterns = extractOptionalStringArray(excludePatterns);
|
|
454
|
-
const extractedIncludeFiles = extractOptionalStringArray(includeFiles);
|
|
455
|
-
|
|
456
|
-
return extractedExcludeDirs && extractedExcludePatterns && extractedIncludeFiles
|
|
457
|
-
? {
|
|
458
|
-
excludeDirs: extractedExcludeDirs,
|
|
459
|
-
excludePatterns: extractedExcludePatterns,
|
|
460
|
-
includeFiles: extractedIncludeFiles,
|
|
461
|
-
}
|
|
462
|
-
: undefined;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
function areGrepOptionsValid<const CountOnly extends boolean>({
|
|
466
|
-
grepOptions,
|
|
467
|
-
}: Readonly<{
|
|
468
|
-
grepOptions: Readonly<Partial<GrepOptions<CountOnly>>>;
|
|
469
|
-
}>) {
|
|
470
|
-
return (
|
|
471
|
-
isValidMaxCount(grepOptions.maxCount) &&
|
|
472
|
-
(grepOptions.cwd == undefined || !!extractString(grepOptions.cwd)) &&
|
|
473
|
-
[
|
|
474
|
-
grepOptions.binary,
|
|
475
|
-
grepOptions.followSymLinks,
|
|
476
|
-
grepOptions.ignoreCase,
|
|
477
|
-
grepOptions.invertMatch,
|
|
478
|
-
grepOptions.printCommand,
|
|
479
|
-
grepOptions.recursive,
|
|
480
|
-
].every(isOptionalBoolean) &&
|
|
481
|
-
isValidPatternSyntax(grepOptions.patternSyntax) &&
|
|
482
|
-
isValidMatchType(grepOptions.matchType) &&
|
|
483
|
-
isValidOutput(grepOptions.output)
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function createSearchPatterns(grepSearchPattern: Readonly<GrepSearchPattern> | undefined) {
|
|
488
|
-
if (!check.isObject(grepSearchPattern)) {
|
|
489
|
-
return [];
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const rawPatterns: unknown[] = check.isArray(grepSearchPattern.patterns)
|
|
493
|
-
? grepSearchPattern.patterns
|
|
494
|
-
: [
|
|
495
|
-
grepSearchPattern.pattern,
|
|
496
|
-
];
|
|
497
|
-
|
|
498
|
-
return extractStringArray(rawPatterns);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function createSearchLocation(
|
|
502
|
-
grepSearchLocation: Readonly<GrepSearchLocation> | undefined,
|
|
503
|
-
): SelectFrom<GrepSearchLocation, {files: true; dirs: true}> | undefined {
|
|
504
|
-
if (!check.isObject(grepSearchLocation)) {
|
|
505
|
-
return undefined;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
const file = extractString(grepSearchLocation.file);
|
|
509
|
-
const dir = extractString(grepSearchLocation.dir);
|
|
510
|
-
|
|
511
|
-
if (grepSearchLocation.files != undefined) {
|
|
512
|
-
return {
|
|
513
|
-
files: extractStringArray(grepSearchLocation.files),
|
|
514
|
-
};
|
|
515
|
-
} else if (file) {
|
|
516
|
-
return {
|
|
517
|
-
files: [
|
|
518
|
-
file,
|
|
519
|
-
],
|
|
520
|
-
};
|
|
521
|
-
} else if (grepSearchLocation.dirs != undefined) {
|
|
522
|
-
return {
|
|
523
|
-
dirs: extractStringArray(grepSearchLocation.dirs),
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
return dir
|
|
528
|
-
? {
|
|
529
|
-
dirs: [
|
|
530
|
-
dir,
|
|
531
|
-
],
|
|
532
|
-
}
|
|
533
|
-
: undefined;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function resolveSearchPart({
|
|
537
|
-
cwd,
|
|
538
|
-
searchPart,
|
|
539
|
-
}: Readonly<{
|
|
540
|
-
cwd: string | undefined;
|
|
541
|
-
searchPart: string;
|
|
542
|
-
}>) {
|
|
543
|
-
return cwd && !isAbsolute(searchPart) ? resolve(cwd, searchPart) : searchPart;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
async function shouldSearchPart({
|
|
547
|
-
cwd,
|
|
548
|
-
followSymLinks,
|
|
549
|
-
includeDirectories,
|
|
550
|
-
searchPart,
|
|
551
|
-
}: Readonly<{
|
|
552
|
-
cwd: string | undefined;
|
|
553
|
-
followSymLinks: boolean | undefined;
|
|
554
|
-
includeDirectories: boolean;
|
|
555
|
-
searchPart: string;
|
|
556
|
-
}>) {
|
|
557
|
-
try {
|
|
558
|
-
const fileStats = await (followSymLinks ? stat : lstat)(
|
|
559
|
-
resolveSearchPart({
|
|
560
|
-
cwd,
|
|
561
|
-
searchPart,
|
|
562
|
-
}),
|
|
563
|
-
);
|
|
564
|
-
|
|
565
|
-
return fileStats.isFile() || (includeDirectories && fileStats.isDirectory());
|
|
566
|
-
} catch {
|
|
567
|
-
return false;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
async function filterSearchParts({
|
|
572
|
-
cwd,
|
|
573
|
-
followSymLinks,
|
|
574
|
-
includeDirectories,
|
|
575
|
-
searchParts,
|
|
576
|
-
}: Readonly<{
|
|
577
|
-
cwd: string | undefined;
|
|
578
|
-
followSymLinks: boolean | undefined;
|
|
579
|
-
includeDirectories: boolean;
|
|
580
|
-
searchParts: ReadonlyArray<string>;
|
|
581
|
-
}>) {
|
|
582
|
-
return (
|
|
583
|
-
await awaitedBlockingMap(searchParts, async (searchPart) => {
|
|
584
|
-
return (await shouldSearchPart({
|
|
585
|
-
cwd,
|
|
586
|
-
followSymLinks,
|
|
587
|
-
includeDirectories,
|
|
588
|
-
searchPart,
|
|
589
|
-
}))
|
|
590
|
-
? searchPart
|
|
591
|
-
: undefined;
|
|
592
|
-
})
|
|
593
|
-
).filter(check.isTruthy);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
async function readDirectDirSearchParts({
|
|
597
|
-
cwd,
|
|
598
|
-
dir,
|
|
599
|
-
followSymLinks,
|
|
600
|
-
}: Readonly<{
|
|
601
|
-
cwd: string | undefined;
|
|
602
|
-
dir: string;
|
|
603
|
-
followSymLinks: boolean | undefined;
|
|
604
|
-
}>) {
|
|
605
|
-
try {
|
|
606
|
-
const readDirPath = resolveSearchPart({
|
|
607
|
-
cwd,
|
|
608
|
-
searchPart: dir,
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
return (
|
|
612
|
-
await awaitedBlockingMap(
|
|
613
|
-
(await readdir(readDirPath)).toSorted().filter((entry) => !entry.startsWith('.')),
|
|
614
|
-
async (entry) => {
|
|
615
|
-
const searchPart = join(dir, entry);
|
|
616
|
-
|
|
617
|
-
return (await shouldSearchPart({
|
|
618
|
-
cwd: readDirPath,
|
|
619
|
-
followSymLinks,
|
|
620
|
-
includeDirectories: false,
|
|
621
|
-
searchPart: entry,
|
|
622
|
-
}))
|
|
623
|
-
? searchPart
|
|
624
|
-
: undefined;
|
|
625
|
-
},
|
|
626
|
-
)
|
|
627
|
-
).filter(check.isTruthy);
|
|
628
|
-
/* node:coverage ignore next 3 */
|
|
629
|
-
} catch {
|
|
630
|
-
return [];
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
async function createSearchParts({
|
|
635
|
-
cwd,
|
|
636
|
-
followSymLinks,
|
|
637
|
-
recursive,
|
|
638
|
-
searchLocation,
|
|
639
|
-
}: Readonly<{
|
|
640
|
-
cwd: string | undefined;
|
|
641
|
-
followSymLinks: boolean | undefined;
|
|
642
|
-
recursive: boolean | undefined;
|
|
643
|
-
searchLocation: SelectFrom<GrepSearchLocation, {files: true; dirs: true}>;
|
|
644
|
-
}>) {
|
|
645
|
-
const searchParts = searchLocation.dirs || searchLocation.files;
|
|
646
|
-
assert.isDefined(searchParts, 'Grep search location was not resolved.');
|
|
647
|
-
|
|
648
|
-
const filteredSearchParts = await filterSearchParts({
|
|
649
|
-
cwd,
|
|
650
|
-
followSymLinks,
|
|
651
|
-
includeDirectories: !!searchLocation.dirs,
|
|
652
|
-
searchParts,
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
return searchLocation.dirs
|
|
656
|
-
? recursive
|
|
657
|
-
? filteredSearchParts
|
|
658
|
-
: (
|
|
659
|
-
await awaitedBlockingMap(filteredSearchParts, (dir) =>
|
|
660
|
-
readDirectDirSearchParts({
|
|
661
|
-
cwd,
|
|
662
|
-
dir,
|
|
663
|
-
followSymLinks,
|
|
664
|
-
}),
|
|
665
|
-
)
|
|
666
|
-
).flat()
|
|
667
|
-
: filteredSearchParts;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
type GrepCountEntryParams = {
|
|
671
|
-
countString: string | undefined;
|
|
672
|
-
fileName: string | undefined;
|
|
673
|
-
};
|
|
674
|
-
|
|
675
|
-
type NullDelimitedGrepRecord = {
|
|
676
|
-
fileName: string;
|
|
677
|
-
value: string;
|
|
678
|
-
};
|
|
679
|
-
|
|
680
|
-
function createGrepCountEntry({countString, fileName}: Readonly<GrepCountEntryParams>) {
|
|
681
|
-
assert.isDefined(fileName, 'Failed parse grep file name.');
|
|
682
|
-
|
|
683
|
-
const count = Number(countString);
|
|
684
|
-
|
|
685
|
-
assert.isNumber(count, `Failed to parse grep number from: '${countString}'`);
|
|
686
|
-
/* node:coverage ignore next 3 */
|
|
687
|
-
if (!count) {
|
|
688
|
-
return undefined;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
return {
|
|
692
|
-
key: fileName,
|
|
693
|
-
value: count,
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function parseNullDelimitedGrepRecords({stdout}: Readonly<{stdout: string}>) {
|
|
698
|
-
const records: NullDelimitedGrepRecord[] = [];
|
|
699
|
-
let recordStartIndex = 0;
|
|
700
|
-
|
|
701
|
-
while (recordStartIndex < stdout.length) {
|
|
702
|
-
const delimiterIndex = stdout.indexOf('\0', recordStartIndex);
|
|
703
|
-
|
|
704
|
-
/* node:coverage ignore next 3 */
|
|
705
|
-
if (delimiterIndex < 0) {
|
|
706
|
-
break;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
const valueStartIndex = delimiterIndex + 1;
|
|
710
|
-
const newlineIndex = stdout.indexOf('\n', valueStartIndex);
|
|
711
|
-
/* node:coverage ignore next */
|
|
712
|
-
const valueEndIndex = newlineIndex < 0 ? stdout.length : newlineIndex;
|
|
713
|
-
|
|
714
|
-
records.push({
|
|
715
|
-
fileName: stdout.slice(recordStartIndex, delimiterIndex),
|
|
716
|
-
value: stdout.slice(valueStartIndex, valueEndIndex),
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
/* node:coverage ignore next */
|
|
720
|
-
recordStartIndex = newlineIndex < 0 ? stdout.length : newlineIndex + 1;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
return records;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
/* node:coverage ignore next 26 */
|
|
727
|
-
function parseColonDelimitedGrepCountOutput(stdout: string) {
|
|
728
|
-
return typedObjectFromEntries(
|
|
729
|
-
stdout
|
|
730
|
-
.trimEnd()
|
|
731
|
-
.split('\n')
|
|
732
|
-
.map((entry) => {
|
|
733
|
-
if (!entry) {
|
|
734
|
-
return undefined;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
const countDelimiterIndex = entry.lastIndexOf(':');
|
|
738
|
-
|
|
739
|
-
return createGrepCountEntry({
|
|
740
|
-
countString:
|
|
741
|
-
countDelimiterIndex < 0 ? undefined : entry.slice(countDelimiterIndex + 1),
|
|
742
|
-
fileName:
|
|
743
|
-
countDelimiterIndex < 0 ? undefined : entry.slice(0, countDelimiterIndex),
|
|
744
|
-
});
|
|
745
|
-
})
|
|
746
|
-
.filter(check.isTruthy)
|
|
747
|
-
.map((entry) => [
|
|
748
|
-
entry.key,
|
|
749
|
-
entry.value,
|
|
750
|
-
]),
|
|
751
|
-
);
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
/* node:coverage ignore next 20 */
|
|
755
|
-
function parseGrepCountOutput(stdout: string) {
|
|
756
|
-
return stdout.includes('\0')
|
|
757
|
-
? typedObjectFromEntries(
|
|
758
|
-
parseNullDelimitedGrepRecords({
|
|
759
|
-
stdout,
|
|
760
|
-
})
|
|
761
|
-
.map((record) => {
|
|
762
|
-
return createGrepCountEntry({
|
|
763
|
-
countString: record.value,
|
|
764
|
-
fileName: record.fileName,
|
|
765
|
-
});
|
|
766
|
-
})
|
|
767
|
-
.filter(check.isTruthy)
|
|
768
|
-
.map((entry) => [
|
|
769
|
-
entry.key,
|
|
770
|
-
entry.value,
|
|
771
|
-
]),
|
|
772
|
-
)
|
|
773
|
-
: parseColonDelimitedGrepCountOutput(stdout);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
/* node:coverage ignore next 7 */
|
|
777
|
-
function tryParseGrepCountOutput(stdout: string) {
|
|
778
|
-
try {
|
|
779
|
-
return parseGrepCountOutput(stdout);
|
|
780
|
-
} catch {
|
|
781
|
-
return undefined;
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function parseKnownFileCountOutput({
|
|
786
|
-
filePath,
|
|
787
|
-
stdout,
|
|
788
|
-
}: Readonly<{
|
|
789
|
-
filePath: string;
|
|
790
|
-
stdout: string;
|
|
791
|
-
}>) {
|
|
792
|
-
/* node:coverage ignore next 3 */
|
|
793
|
-
if (stdout.includes('\0')) {
|
|
794
|
-
return parseGrepCountOutput(stdout)[filePath];
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
const countPrefix = `${filePath}:`;
|
|
798
|
-
/* node:coverage ignore next */
|
|
799
|
-
const outputLine = stdout.endsWith('\n') ? stdout.slice(0, -1) : stdout;
|
|
800
|
-
|
|
801
|
-
/* node:coverage ignore next 3 */
|
|
802
|
-
if (!outputLine.startsWith(countPrefix)) {
|
|
803
|
-
return undefined;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
/* node:coverage ignore next */
|
|
807
|
-
return createGrepCountEntry({
|
|
808
|
-
countString: outputLine.slice(countPrefix.length),
|
|
809
|
-
fileName: filePath,
|
|
810
|
-
})?.value;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
async function runKnownFileGrepCount({
|
|
814
|
-
cwd,
|
|
815
|
-
filePath,
|
|
816
|
-
grepArgs,
|
|
817
|
-
}: Readonly<{
|
|
818
|
-
cwd: string | undefined;
|
|
819
|
-
filePath: string;
|
|
820
|
-
grepArgs: ReadonlyArray<string>;
|
|
821
|
-
}>) {
|
|
822
|
-
const result = await runGrepCommand({
|
|
823
|
-
args: replaceGrepSearchOperands({
|
|
824
|
-
args: grepArgs,
|
|
825
|
-
searchParts: [
|
|
826
|
-
filePath,
|
|
827
|
-
],
|
|
828
|
-
}),
|
|
829
|
-
cwd,
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
/* node:coverage ignore next 3 */
|
|
833
|
-
if (didGrepFail(result)) {
|
|
834
|
-
return undefined;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
const count = parseKnownFileCountOutput({
|
|
838
|
-
filePath,
|
|
839
|
-
stdout: result.stdout,
|
|
840
|
-
});
|
|
841
|
-
|
|
842
|
-
/* node:coverage ignore next 3 */
|
|
843
|
-
if (!count) {
|
|
844
|
-
return undefined;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
return {
|
|
848
|
-
key: filePath,
|
|
849
|
-
value: count,
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
async function runGrepCountFallback({
|
|
854
|
-
cwd,
|
|
855
|
-
grepArgs,
|
|
856
|
-
}: Readonly<{
|
|
857
|
-
cwd: string | undefined;
|
|
858
|
-
grepArgs: ReadonlyArray<string>;
|
|
859
|
-
}>) {
|
|
860
|
-
const filesOnlyResult = await runGrepCommand({
|
|
861
|
-
args: replaceGrepCountOutputArg({
|
|
862
|
-
args: grepArgs,
|
|
863
|
-
replacement: '--files-with-matches',
|
|
864
|
-
}),
|
|
865
|
-
cwd,
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
/* node:coverage ignore next 3 */
|
|
869
|
-
if (didGrepFail(filesOnlyResult) || filesOnlyResult.exitCode === 1 || !filesOnlyResult.stdout) {
|
|
870
|
-
return {};
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
return typedObjectFromEntries(
|
|
874
|
-
(
|
|
875
|
-
await awaitedBlockingMap(
|
|
876
|
-
getObjectTypedKeys(parseGrepFilesOnlyOutput(filesOnlyResult.stdout)),
|
|
877
|
-
(filePath) => {
|
|
878
|
-
return runKnownFileGrepCount({
|
|
879
|
-
cwd,
|
|
880
|
-
filePath,
|
|
881
|
-
grepArgs,
|
|
882
|
-
});
|
|
883
|
-
},
|
|
884
|
-
)
|
|
885
|
-
)
|
|
886
|
-
.filter(check.isTruthy)
|
|
887
|
-
.map((entry) => [
|
|
888
|
-
entry.key,
|
|
889
|
-
entry.value,
|
|
890
|
-
]),
|
|
891
|
-
);
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
function parseGrepFilesOnlyOutput(stdout: string) {
|
|
895
|
-
return typedObjectFromEntries(
|
|
896
|
-
/* node:coverage ignore next */
|
|
897
|
-
(stdout.includes('\0') ? stdout.split('\0') : stdout.trimEnd().split('\n'))
|
|
898
|
-
.filter(check.isTruthy)
|
|
899
|
-
.map((entry) => [
|
|
900
|
-
entry,
|
|
901
|
-
[],
|
|
902
|
-
]),
|
|
903
|
-
);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
function parseGrepNormalOutput(stdout: string) {
|
|
907
|
-
const fileMatches = new Map<string, string[]>();
|
|
908
|
-
|
|
909
|
-
parseNullDelimitedGrepRecords({
|
|
910
|
-
stdout,
|
|
911
|
-
}).forEach((record) => {
|
|
912
|
-
fileMatches.set(record.fileName, [
|
|
913
|
-
...(fileMatches.get(record.fileName) || []),
|
|
914
|
-
record.value,
|
|
915
|
-
]);
|
|
916
|
-
});
|
|
917
|
-
|
|
918
|
-
return typedObjectFromEntries([...fileMatches.entries()]);
|
|
919
|
-
}
|
|
920
|
-
|
|
921
201
|
/**
|
|
922
|
-
* Output of {@link grep}. Each key is
|
|
923
|
-
*
|
|
202
|
+
* Output of {@link grep}. Each key is an absolute file path. Values are array of matches lines for
|
|
203
|
+
* that file.
|
|
924
204
|
*
|
|
925
205
|
* @category Internal
|
|
926
206
|
* @category Package : @augment-vir/node
|
|
@@ -945,24 +225,32 @@ export async function grep<const CountOnly extends boolean = false>(
|
|
|
945
225
|
grepSearchLocation: Readonly<GrepSearchLocation>,
|
|
946
226
|
options: Readonly<GrepOptions<CountOnly>> = {},
|
|
947
227
|
): Promise<GrepMatches<CountOnly>> {
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
const searchPatterns = createSearchPatterns(grepSearchPattern);
|
|
952
|
-
const grepOptionArrays = extractGrepOptionArrays(grepOptions);
|
|
953
|
-
const cwd = extractString(grepOptions.cwd);
|
|
228
|
+
const searchPatterns: string[] = (
|
|
229
|
+
grepSearchPattern.patterns || [grepSearchPattern.pattern]
|
|
230
|
+
).filter(check.isTruthy);
|
|
954
231
|
|
|
955
|
-
if (
|
|
956
|
-
!searchPatterns.length ||
|
|
957
|
-
!grepOptionArrays ||
|
|
958
|
-
!areGrepOptionsValid({
|
|
959
|
-
grepOptions,
|
|
960
|
-
})
|
|
961
|
-
) {
|
|
232
|
+
if (!searchPatterns.length) {
|
|
962
233
|
return {};
|
|
963
234
|
}
|
|
964
235
|
|
|
965
|
-
const searchLocation
|
|
236
|
+
const searchLocation: SelectFrom<GrepSearchLocation, {files: true; dirs: true}> | undefined =
|
|
237
|
+
grepSearchLocation.files
|
|
238
|
+
? {
|
|
239
|
+
files: grepSearchLocation.files,
|
|
240
|
+
}
|
|
241
|
+
: grepSearchLocation.file
|
|
242
|
+
? {
|
|
243
|
+
files: [grepSearchLocation.file],
|
|
244
|
+
}
|
|
245
|
+
: grepSearchLocation.dirs
|
|
246
|
+
? {
|
|
247
|
+
dirs: grepSearchLocation.dirs,
|
|
248
|
+
}
|
|
249
|
+
: grepSearchLocation.dir
|
|
250
|
+
? {
|
|
251
|
+
dirs: [grepSearchLocation.dir],
|
|
252
|
+
}
|
|
253
|
+
: undefined;
|
|
966
254
|
|
|
967
255
|
if (
|
|
968
256
|
!searchLocation ||
|
|
@@ -972,103 +260,140 @@ export async function grep<const CountOnly extends boolean = false>(
|
|
|
972
260
|
return {};
|
|
973
261
|
}
|
|
974
262
|
|
|
975
|
-
const searchParts =
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
searchLocation
|
|
980
|
-
});
|
|
981
|
-
|
|
982
|
-
if (!searchParts.length) {
|
|
983
|
-
return {};
|
|
984
|
-
}
|
|
263
|
+
const searchParts = searchLocation.dirs
|
|
264
|
+
? options.recursive
|
|
265
|
+
? searchLocation.dirs
|
|
266
|
+
: searchLocation.dirs.map((dir) => join(dir, '*'))
|
|
267
|
+
: searchLocation.files;
|
|
985
268
|
|
|
986
|
-
const
|
|
987
|
-
|
|
269
|
+
const fullCommand = [
|
|
270
|
+
'grep',
|
|
271
|
+
options.patternSyntax?.basicRegExp
|
|
988
272
|
? '--basic-regexp'
|
|
989
|
-
:
|
|
273
|
+
: options.patternSyntax?.extendedRegExp
|
|
990
274
|
? '--extended-regexp'
|
|
991
|
-
:
|
|
275
|
+
: options.patternSyntax?.fixedStrings
|
|
992
276
|
? '--fixed-strings'
|
|
993
277
|
: '',
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
278
|
+
options.ignoreCase ? '--ignore-case' : '',
|
|
279
|
+
options.invertMatch && !options.output?.filesOnly ? '--invert-match' : '',
|
|
280
|
+
options.matchType?.wordRegExp
|
|
997
281
|
? '--word-regexp'
|
|
998
|
-
:
|
|
282
|
+
: options.matchType?.lineRegExp
|
|
999
283
|
? '--line-regexp'
|
|
1000
284
|
: '',
|
|
1001
|
-
|
|
285
|
+
options.output?.countOnly
|
|
1002
286
|
? '--count'
|
|
1003
|
-
:
|
|
1004
|
-
?
|
|
287
|
+
: options.output?.filesOnly
|
|
288
|
+
? options.invertMatch
|
|
1005
289
|
? '--files-without-match'
|
|
1006
290
|
: '--files-with-matches'
|
|
1007
291
|
: '',
|
|
1008
292
|
'--color=never',
|
|
1009
|
-
|
|
293
|
+
options.maxCount ? `--max-count=${options.maxCount}` : '',
|
|
1010
294
|
'--no-messages',
|
|
1011
|
-
'--devices=skip',
|
|
1012
295
|
'--with-filename',
|
|
1013
296
|
'--null',
|
|
1014
|
-
...(
|
|
1015
|
-
?
|
|
1016
|
-
(excludePattern) => `--exclude
|
|
297
|
+
...(options.excludePatterns?.length
|
|
298
|
+
? options.excludePatterns.map(
|
|
299
|
+
(excludePattern) => `--exclude="${escape(excludePattern)}"`,
|
|
1017
300
|
)
|
|
1018
301
|
: []),
|
|
1019
|
-
recursiveFlag(
|
|
1020
|
-
...(
|
|
1021
|
-
?
|
|
302
|
+
recursiveFlag(options),
|
|
303
|
+
...(options.excludeDirs?.length
|
|
304
|
+
? options.excludeDirs.map((excludeDir) => `--exclude-dir="${escape(excludeDir)}"`)
|
|
1022
305
|
: []),
|
|
1023
|
-
...(
|
|
1024
|
-
?
|
|
306
|
+
...(options.includeFiles?.length
|
|
307
|
+
? options.includeFiles.map((includeFile) => `--include="${escape(includeFile)}"`)
|
|
1025
308
|
: []),
|
|
1026
|
-
|
|
1027
|
-
...searchPatterns.
|
|
1028
|
-
'-e',
|
|
1029
|
-
searchPattern,
|
|
1030
|
-
]),
|
|
1031
|
-
'--',
|
|
309
|
+
options.binary ? '--binary' : '',
|
|
310
|
+
...searchPatterns.map((searchPattern) => `-e "${searchPattern}"`),
|
|
1032
311
|
...searchParts,
|
|
1033
|
-
]
|
|
312
|
+
]
|
|
313
|
+
.filter(check.isTruthy)
|
|
314
|
+
.join(' ');
|
|
1034
315
|
|
|
1035
|
-
if (
|
|
1036
|
-
log.faint(`> ${
|
|
316
|
+
if (options.printCommand) {
|
|
317
|
+
log.faint(`> ${fullCommand}`);
|
|
1037
318
|
}
|
|
1038
319
|
|
|
1039
|
-
const result = await
|
|
1040
|
-
|
|
1041
|
-
cwd,
|
|
320
|
+
const result = await runShellCommand(fullCommand, {
|
|
321
|
+
cwd: options.cwd,
|
|
1042
322
|
});
|
|
1043
323
|
|
|
1044
|
-
|
|
324
|
+
const trimmedOutput = result.stdout.trim();
|
|
325
|
+
|
|
326
|
+
if (result.exitCode === 1 || !trimmedOutput) {
|
|
1045
327
|
/** No matches. */
|
|
1046
328
|
return {};
|
|
1047
|
-
} else if (
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
:
|
|
329
|
+
} else if (options.output?.countOnly) {
|
|
330
|
+
return arrayToObject(
|
|
331
|
+
trimmedOutput.split('\n'),
|
|
332
|
+
(entry) => {
|
|
333
|
+
/** Ignore empty strings. */
|
|
334
|
+
/* node:coverage ignore next 3 */
|
|
335
|
+
if (!entry) {
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* GNU `grep` (Linux) separates the file name from its count with a null byte when
|
|
341
|
+
* `--null` is set, while BSD `grep` (macOS) uses a colon. Accept either.
|
|
342
|
+
*/
|
|
343
|
+
const [
|
|
344
|
+
,
|
|
345
|
+
fileName,
|
|
346
|
+
countString,
|
|
347
|
+
] = safeMatch(entry, /^(.+)[\0:](\d+)$/);
|
|
348
|
+
|
|
349
|
+
assert.isDefined(fileName, `Failed parse grep file name from: '${entry}'`);
|
|
1053
350
|
|
|
1054
|
-
|
|
1055
|
-
if (parsedCountOutput) {
|
|
1056
|
-
return parsedCountOutput satisfies Record<string, number> as GrepMatches<CountOnly>;
|
|
1057
|
-
}
|
|
351
|
+
const count = Number(countString);
|
|
1058
352
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
353
|
+
assert.isNumber(count, `Failed to parse grep number from: '${entry}'`);
|
|
354
|
+
if (!count) {
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
key: fileName,
|
|
360
|
+
value: count,
|
|
361
|
+
};
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
useRequired: true,
|
|
365
|
+
},
|
|
366
|
+
) satisfies Record<string, number> as GrepMatches<CountOnly>;
|
|
367
|
+
} else if (options.output?.filesOnly) {
|
|
368
|
+
return arrayToObject(
|
|
369
|
+
trimmedOutput.split(/[\0\n]/),
|
|
370
|
+
(entry) => {
|
|
371
|
+
/** Ignore empty strings. */
|
|
372
|
+
if (!entry) {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
key: entry,
|
|
378
|
+
value: [],
|
|
379
|
+
};
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
useRequired: true,
|
|
383
|
+
},
|
|
384
|
+
) satisfies Record<string, string[]> as GrepMatches as GrepMatches<CountOnly>;
|
|
1068
385
|
} else {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
386
|
+
const outputLines = trimmedOutput.split(/[\0\n]/);
|
|
387
|
+
|
|
388
|
+
const fileMatches: Record<string, string[]> = {};
|
|
389
|
+
|
|
390
|
+
outputLines.forEach((line, index) => {
|
|
391
|
+
if (!(index % 2)) {
|
|
392
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
393
|
+
getOrSet(fileMatches, line, () => []).push(outputLines[index + 1]!);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
return fileMatches as GrepMatches<CountOnly>;
|
|
1073
398
|
}
|
|
1074
399
|
}
|