@augment-vir/node 31.73.4 → 32.0.0

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.
@@ -1,10 +1,16 @@
1
1
  import { assert, check } from '@augment-vir/assert';
2
- import { arrayToObject, getOrSet, log, safeMatch, } from '@augment-vir/common';
3
- import { join } from 'node:path';
2
+ import { awaitedBlockingMap, getObjectTypedKeys, log, typedObjectFromEntries, } from '@augment-vir/common';
3
+ import { spawn } from 'node:child_process';
4
+ import { lstat, readdir, stat } from 'node:fs/promises';
5
+ import { isAbsolute, join, resolve } from 'node:path';
4
6
  import { isOperatingSystem, OperatingSystem } from '../os/operating-system.js';
5
- import { runShellCommand } from '../terminal/shell.js';
6
- function escape(input) {
7
- return input.replaceAll('"', String.raw `\"`).replaceAll('\n', '');
7
+ const grepBinPath = '/usr/bin/grep';
8
+ function shellQuote(input) {
9
+ return [
10
+ "'",
11
+ input.replaceAll("'", String.raw `'\''`),
12
+ "'",
13
+ ].join('');
8
14
  }
9
15
  function recursiveFlag({ recursive, followSymLinks, }) {
10
16
  if (!recursive) {
@@ -21,6 +27,484 @@ function recursiveFlag({ recursive, followSymLinks, }) {
21
27
  /* node:coverage ignore next */
22
28
  return isOperatingSystem(OperatingSystem.Mac) ? '-RS' : '-R';
23
29
  }
30
+ function isValidMaxCount(maxCount) {
31
+ return (maxCount == undefined ||
32
+ (check.isNumber(maxCount) && Number.isInteger(maxCount) && maxCount >= -1));
33
+ }
34
+ function isOptionalBoolean(input) {
35
+ return input == undefined || check.isBoolean(input);
36
+ }
37
+ function isValidTrueOnlyOptionGroup({ input, values, }) {
38
+ return (input == undefined ||
39
+ (check.isObject(input) &&
40
+ values.every((value) => value == undefined || value === true) &&
41
+ values.filter((value) => value === true).length === 1));
42
+ }
43
+ function isValidPatternSyntax(input) {
44
+ return isValidTrueOnlyOptionGroup({
45
+ input,
46
+ values: check.isObject(input)
47
+ ? [
48
+ input.basicRegExp,
49
+ input.extendedRegExp,
50
+ input.fixedStrings,
51
+ ]
52
+ : [],
53
+ });
54
+ }
55
+ function isValidMatchType(input) {
56
+ return isValidTrueOnlyOptionGroup({
57
+ input,
58
+ values: check.isObject(input)
59
+ ? [
60
+ input.lineRegExp,
61
+ input.wordRegExp,
62
+ ]
63
+ : [],
64
+ });
65
+ }
66
+ function isValidOutput(input) {
67
+ return isValidTrueOnlyOptionGroup({
68
+ input,
69
+ values: check.isObject(input)
70
+ ? [
71
+ input.countOnly,
72
+ input.filesOnly,
73
+ ]
74
+ : [],
75
+ });
76
+ }
77
+ function didGrepFail({ exitCode }) {
78
+ return exitCode == undefined || exitCode > 1;
79
+ }
80
+ function spawnGrepProcess({ args, cwd, }) {
81
+ try {
82
+ return spawn(grepBinPath, args, {
83
+ cwd,
84
+ stdio: [
85
+ 'ignore',
86
+ 'pipe',
87
+ 'pipe',
88
+ ],
89
+ });
90
+ /* node:coverage ignore next 3 */
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ }
96
+ async function runGrepCommand({ args, cwd, }) {
97
+ return new Promise((resolveOutput) => {
98
+ const stdoutChunks = [];
99
+ const grepProcess = spawnGrepProcess({
100
+ args,
101
+ cwd,
102
+ });
103
+ if (!grepProcess) {
104
+ resolveOutput({
105
+ exitCode: undefined,
106
+ stdout: '',
107
+ });
108
+ return;
109
+ }
110
+ assert.isDefined(grepProcess.stdout, 'stdout emitter was not created for grep.');
111
+ assert.isDefined(grepProcess.stderr, 'stderr emitter was not created for grep.');
112
+ grepProcess.stdout.on('data', (chunk) => {
113
+ stdoutChunks.push(Buffer.from(chunk));
114
+ });
115
+ grepProcess.stderr.on('data', () => { });
116
+ /* node:coverage ignore next 5 */
117
+ grepProcess.on('error', () => {
118
+ resolveOutput({
119
+ exitCode: undefined,
120
+ stdout: Buffer.concat(stdoutChunks).toString(),
121
+ });
122
+ });
123
+ grepProcess.on('close', (rawExitCode) => {
124
+ resolveOutput({
125
+ /* node:coverage ignore next */
126
+ exitCode: rawExitCode ?? undefined,
127
+ stdout: Buffer.concat(stdoutChunks).toString(),
128
+ });
129
+ });
130
+ });
131
+ }
132
+ function redactGrepArgForLogging({ arg, index, operandDelimiterIndex, previousArg, }) {
133
+ if (previousArg === '-e') {
134
+ return '<pattern>';
135
+ }
136
+ else if (operandDelimiterIndex >= 0 && index > operandDelimiterIndex) {
137
+ return '<path>';
138
+ }
139
+ else if (arg.startsWith('--exclude-dir=')) {
140
+ return '--exclude-dir=<glob>';
141
+ }
142
+ else if (arg.startsWith('--exclude=')) {
143
+ return '--exclude=<glob>';
144
+ }
145
+ else if (arg.startsWith('--include=')) {
146
+ return '--include=<glob>';
147
+ }
148
+ else if (arg.startsWith('--max-count=')) {
149
+ return '--max-count=<count>';
150
+ }
151
+ else {
152
+ return arg;
153
+ }
154
+ }
155
+ function formatGrepCommand(args) {
156
+ const operandDelimiterIndex = args.indexOf('--');
157
+ return [
158
+ 'grep',
159
+ ...args.map((arg, index) => shellQuote(redactGrepArgForLogging({
160
+ arg,
161
+ index,
162
+ operandDelimiterIndex,
163
+ previousArg: args[index - 1],
164
+ }))),
165
+ ].join(' ');
166
+ }
167
+ function replaceGrepCountOutputArg({ args, replacement, }) {
168
+ const countArgIndex = args.indexOf('--count');
169
+ /* node:coverage ignore next */
170
+ return countArgIndex < 0 ? [...args] : args.toSpliced(countArgIndex, 1, replacement);
171
+ }
172
+ function replaceGrepSearchOperands({ args, searchParts, }) {
173
+ const operandDelimiterIndex = args.indexOf('--');
174
+ return [
175
+ ...args.slice(0, operandDelimiterIndex + 1),
176
+ ...searchParts,
177
+ ];
178
+ }
179
+ function extractStringArray(input) {
180
+ return check.isArray(input) ? input.filter(check.isString).filter(check.isTruthy) : [];
181
+ }
182
+ function extractOptionalStringArray(input) {
183
+ if (input == undefined) {
184
+ return [];
185
+ }
186
+ else if (!check.isArray(input) || !input.every(check.isString)) {
187
+ return undefined;
188
+ }
189
+ return input.filter(check.isTruthy);
190
+ }
191
+ function extractString(input) {
192
+ return check.isString(input) && input ? input : undefined;
193
+ }
194
+ function extractGrepOptionArrays({ excludeDirs, excludePatterns, includeFiles, }) {
195
+ const extractedExcludeDirs = extractOptionalStringArray(excludeDirs);
196
+ const extractedExcludePatterns = extractOptionalStringArray(excludePatterns);
197
+ const extractedIncludeFiles = extractOptionalStringArray(includeFiles);
198
+ return extractedExcludeDirs && extractedExcludePatterns && extractedIncludeFiles
199
+ ? {
200
+ excludeDirs: extractedExcludeDirs,
201
+ excludePatterns: extractedExcludePatterns,
202
+ includeFiles: extractedIncludeFiles,
203
+ }
204
+ : undefined;
205
+ }
206
+ function areGrepOptionsValid({ grepOptions, }) {
207
+ return (isValidMaxCount(grepOptions.maxCount) &&
208
+ (grepOptions.cwd == undefined || !!extractString(grepOptions.cwd)) &&
209
+ [
210
+ grepOptions.binary,
211
+ grepOptions.followSymLinks,
212
+ grepOptions.ignoreCase,
213
+ grepOptions.invertMatch,
214
+ grepOptions.printCommand,
215
+ grepOptions.recursive,
216
+ ].every(isOptionalBoolean) &&
217
+ isValidPatternSyntax(grepOptions.patternSyntax) &&
218
+ isValidMatchType(grepOptions.matchType) &&
219
+ isValidOutput(grepOptions.output));
220
+ }
221
+ function createSearchPatterns(grepSearchPattern) {
222
+ if (!check.isObject(grepSearchPattern)) {
223
+ return [];
224
+ }
225
+ const rawPatterns = check.isArray(grepSearchPattern.patterns)
226
+ ? grepSearchPattern.patterns
227
+ : [
228
+ grepSearchPattern.pattern,
229
+ ];
230
+ return extractStringArray(rawPatterns);
231
+ }
232
+ function createSearchLocation(grepSearchLocation) {
233
+ if (!check.isObject(grepSearchLocation)) {
234
+ return undefined;
235
+ }
236
+ const file = extractString(grepSearchLocation.file);
237
+ const dir = extractString(grepSearchLocation.dir);
238
+ if (grepSearchLocation.files != undefined) {
239
+ return {
240
+ files: extractStringArray(grepSearchLocation.files),
241
+ };
242
+ }
243
+ else if (file) {
244
+ return {
245
+ files: [
246
+ file,
247
+ ],
248
+ };
249
+ }
250
+ else if (grepSearchLocation.dirs != undefined) {
251
+ return {
252
+ dirs: extractStringArray(grepSearchLocation.dirs),
253
+ };
254
+ }
255
+ return dir
256
+ ? {
257
+ dirs: [
258
+ dir,
259
+ ],
260
+ }
261
+ : undefined;
262
+ }
263
+ function resolveSearchPart({ cwd, searchPart, }) {
264
+ return cwd && !isAbsolute(searchPart) ? resolve(cwd, searchPart) : searchPart;
265
+ }
266
+ async function shouldSearchPart({ cwd, followSymLinks, includeDirectories, searchPart, }) {
267
+ try {
268
+ const fileStats = await (followSymLinks ? stat : lstat)(resolveSearchPart({
269
+ cwd,
270
+ searchPart,
271
+ }));
272
+ return fileStats.isFile() || (includeDirectories && fileStats.isDirectory());
273
+ }
274
+ catch {
275
+ return false;
276
+ }
277
+ }
278
+ async function filterSearchParts({ cwd, followSymLinks, includeDirectories, searchParts, }) {
279
+ return (await awaitedBlockingMap(searchParts, async (searchPart) => {
280
+ return (await shouldSearchPart({
281
+ cwd,
282
+ followSymLinks,
283
+ includeDirectories,
284
+ searchPart,
285
+ }))
286
+ ? searchPart
287
+ : undefined;
288
+ })).filter(check.isTruthy);
289
+ }
290
+ async function readDirectDirSearchParts({ cwd, dir, followSymLinks, }) {
291
+ try {
292
+ const readDirPath = resolveSearchPart({
293
+ cwd,
294
+ searchPart: dir,
295
+ });
296
+ return (await awaitedBlockingMap((await readdir(readDirPath)).toSorted().filter((entry) => !entry.startsWith('.')), async (entry) => {
297
+ const searchPart = join(dir, entry);
298
+ return (await shouldSearchPart({
299
+ cwd: readDirPath,
300
+ followSymLinks,
301
+ includeDirectories: false,
302
+ searchPart: entry,
303
+ }))
304
+ ? searchPart
305
+ : undefined;
306
+ })).filter(check.isTruthy);
307
+ /* node:coverage ignore next 3 */
308
+ }
309
+ catch {
310
+ return [];
311
+ }
312
+ }
313
+ async function createSearchParts({ cwd, followSymLinks, recursive, searchLocation, }) {
314
+ const searchParts = searchLocation.dirs || searchLocation.files;
315
+ assert.isDefined(searchParts, 'Grep search location was not resolved.');
316
+ const filteredSearchParts = await filterSearchParts({
317
+ cwd,
318
+ followSymLinks,
319
+ includeDirectories: !!searchLocation.dirs,
320
+ searchParts,
321
+ });
322
+ return searchLocation.dirs
323
+ ? recursive
324
+ ? filteredSearchParts
325
+ : (await awaitedBlockingMap(filteredSearchParts, (dir) => readDirectDirSearchParts({
326
+ cwd,
327
+ dir,
328
+ followSymLinks,
329
+ }))).flat()
330
+ : filteredSearchParts;
331
+ }
332
+ function createGrepCountEntry({ countString, fileName }) {
333
+ assert.isDefined(fileName, 'Failed parse grep file name.');
334
+ const count = Number(countString);
335
+ assert.isNumber(count, `Failed to parse grep number from: '${countString}'`);
336
+ /* node:coverage ignore next 3 */
337
+ if (!count) {
338
+ return undefined;
339
+ }
340
+ return {
341
+ key: fileName,
342
+ value: count,
343
+ };
344
+ }
345
+ function parseNullDelimitedGrepRecords({ stdout }) {
346
+ const records = [];
347
+ let recordStartIndex = 0;
348
+ while (recordStartIndex < stdout.length) {
349
+ const delimiterIndex = stdout.indexOf('\0', recordStartIndex);
350
+ /* node:coverage ignore next 3 */
351
+ if (delimiterIndex < 0) {
352
+ break;
353
+ }
354
+ const valueStartIndex = delimiterIndex + 1;
355
+ const newlineIndex = stdout.indexOf('\n', valueStartIndex);
356
+ /* node:coverage ignore next */
357
+ const valueEndIndex = newlineIndex < 0 ? stdout.length : newlineIndex;
358
+ records.push({
359
+ fileName: stdout.slice(recordStartIndex, delimiterIndex),
360
+ value: stdout.slice(valueStartIndex, valueEndIndex),
361
+ });
362
+ /* node:coverage ignore next */
363
+ recordStartIndex = newlineIndex < 0 ? stdout.length : newlineIndex + 1;
364
+ }
365
+ return records;
366
+ }
367
+ /* node:coverage ignore next 26 */
368
+ function parseColonDelimitedGrepCountOutput(stdout) {
369
+ return typedObjectFromEntries(stdout
370
+ .trimEnd()
371
+ .split('\n')
372
+ .map((entry) => {
373
+ if (!entry) {
374
+ return undefined;
375
+ }
376
+ const countDelimiterIndex = entry.lastIndexOf(':');
377
+ return createGrepCountEntry({
378
+ countString: countDelimiterIndex < 0 ? undefined : entry.slice(countDelimiterIndex + 1),
379
+ fileName: countDelimiterIndex < 0 ? undefined : entry.slice(0, countDelimiterIndex),
380
+ });
381
+ })
382
+ .filter(check.isTruthy)
383
+ .map((entry) => [
384
+ entry.key,
385
+ entry.value,
386
+ ]));
387
+ }
388
+ /* node:coverage ignore next 20 */
389
+ function parseGrepCountOutput(stdout) {
390
+ return stdout.includes('\0')
391
+ ? typedObjectFromEntries(parseNullDelimitedGrepRecords({
392
+ stdout,
393
+ })
394
+ .map((record) => {
395
+ return createGrepCountEntry({
396
+ countString: record.value,
397
+ fileName: record.fileName,
398
+ });
399
+ })
400
+ .filter(check.isTruthy)
401
+ .map((entry) => [
402
+ entry.key,
403
+ entry.value,
404
+ ]))
405
+ : parseColonDelimitedGrepCountOutput(stdout);
406
+ }
407
+ /* node:coverage ignore next 7 */
408
+ function tryParseGrepCountOutput(stdout) {
409
+ try {
410
+ return parseGrepCountOutput(stdout);
411
+ }
412
+ catch {
413
+ return undefined;
414
+ }
415
+ }
416
+ function parseKnownFileCountOutput({ filePath, stdout, }) {
417
+ /* node:coverage ignore next 3 */
418
+ if (stdout.includes('\0')) {
419
+ return parseGrepCountOutput(stdout)[filePath];
420
+ }
421
+ const countPrefix = `${filePath}:`;
422
+ /* node:coverage ignore next */
423
+ const outputLine = stdout.endsWith('\n') ? stdout.slice(0, -1) : stdout;
424
+ /* node:coverage ignore next 3 */
425
+ if (!outputLine.startsWith(countPrefix)) {
426
+ return undefined;
427
+ }
428
+ /* node:coverage ignore next */
429
+ return createGrepCountEntry({
430
+ countString: outputLine.slice(countPrefix.length),
431
+ fileName: filePath,
432
+ })?.value;
433
+ }
434
+ async function runKnownFileGrepCount({ cwd, filePath, grepArgs, }) {
435
+ const result = await runGrepCommand({
436
+ args: replaceGrepSearchOperands({
437
+ args: grepArgs,
438
+ searchParts: [
439
+ filePath,
440
+ ],
441
+ }),
442
+ cwd,
443
+ });
444
+ /* node:coverage ignore next 3 */
445
+ if (didGrepFail(result)) {
446
+ return undefined;
447
+ }
448
+ const count = parseKnownFileCountOutput({
449
+ filePath,
450
+ stdout: result.stdout,
451
+ });
452
+ /* node:coverage ignore next 3 */
453
+ if (!count) {
454
+ return undefined;
455
+ }
456
+ return {
457
+ key: filePath,
458
+ value: count,
459
+ };
460
+ }
461
+ async function runGrepCountFallback({ cwd, grepArgs, }) {
462
+ const filesOnlyResult = await runGrepCommand({
463
+ args: replaceGrepCountOutputArg({
464
+ args: grepArgs,
465
+ replacement: '--files-with-matches',
466
+ }),
467
+ cwd,
468
+ });
469
+ /* node:coverage ignore next 3 */
470
+ if (didGrepFail(filesOnlyResult) || filesOnlyResult.exitCode === 1 || !filesOnlyResult.stdout) {
471
+ return {};
472
+ }
473
+ return typedObjectFromEntries((await awaitedBlockingMap(getObjectTypedKeys(parseGrepFilesOnlyOutput(filesOnlyResult.stdout)), (filePath) => {
474
+ return runKnownFileGrepCount({
475
+ cwd,
476
+ filePath,
477
+ grepArgs,
478
+ });
479
+ }))
480
+ .filter(check.isTruthy)
481
+ .map((entry) => [
482
+ entry.key,
483
+ entry.value,
484
+ ]));
485
+ }
486
+ function parseGrepFilesOnlyOutput(stdout) {
487
+ return typedObjectFromEntries(
488
+ /* node:coverage ignore next */
489
+ (stdout.includes('\0') ? stdout.split('\0') : stdout.trimEnd().split('\n'))
490
+ .filter(check.isTruthy)
491
+ .map((entry) => [
492
+ entry,
493
+ [],
494
+ ]));
495
+ }
496
+ function parseGrepNormalOutput(stdout) {
497
+ const fileMatches = new Map();
498
+ parseNullDelimitedGrepRecords({
499
+ stdout,
500
+ }).forEach((record) => {
501
+ fileMatches.set(record.fileName, [
502
+ ...(fileMatches.get(record.fileName) || []),
503
+ record.value,
504
+ ]);
505
+ });
506
+ return typedObjectFromEntries([...fileMatches.entries()]);
507
+ }
24
508
  /**
25
509
  * Run `grep`, matching patterns to specific lines in files or directories.
26
510
  *
@@ -29,141 +513,109 @@ function recursiveFlag({ recursive, followSymLinks, }) {
29
513
  * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
30
514
  */
31
515
  export async function grep(grepSearchPattern, grepSearchLocation, options = {}) {
32
- const searchPatterns = (grepSearchPattern.patterns || [grepSearchPattern.pattern]).filter(check.isTruthy);
33
- if (!searchPatterns.length) {
516
+ const grepOptions = check.isObject(options)
517
+ ? options
518
+ : {};
519
+ const searchPatterns = createSearchPatterns(grepSearchPattern);
520
+ const grepOptionArrays = extractGrepOptionArrays(grepOptions);
521
+ const cwd = extractString(grepOptions.cwd);
522
+ if (!searchPatterns.length ||
523
+ !grepOptionArrays ||
524
+ !areGrepOptionsValid({
525
+ grepOptions,
526
+ })) {
34
527
  return {};
35
528
  }
36
- const searchLocation = grepSearchLocation.files
37
- ? {
38
- files: grepSearchLocation.files,
39
- }
40
- : grepSearchLocation.file
41
- ? {
42
- files: [grepSearchLocation.file],
43
- }
44
- : grepSearchLocation.dirs
45
- ? {
46
- dirs: grepSearchLocation.dirs,
47
- }
48
- : grepSearchLocation.dir
49
- ? {
50
- dirs: [grepSearchLocation.dir],
51
- }
52
- : undefined;
529
+ const searchLocation = createSearchLocation(grepSearchLocation);
53
530
  if (!searchLocation ||
54
531
  (searchLocation.dirs && !searchLocation.dirs.length) ||
55
532
  (searchLocation.files && !searchLocation.files.length)) {
56
533
  return {};
57
534
  }
58
- const searchParts = searchLocation.dirs
59
- ? options.recursive
60
- ? searchLocation.dirs
61
- : searchLocation.dirs.map((dir) => join(dir, '*'))
62
- : searchLocation.files;
63
- const fullCommand = [
64
- 'grep',
65
- options.patternSyntax?.basicRegExp
535
+ const searchParts = await createSearchParts({
536
+ cwd,
537
+ followSymLinks: grepOptions.followSymLinks,
538
+ recursive: grepOptions.recursive,
539
+ searchLocation,
540
+ });
541
+ if (!searchParts.length) {
542
+ return {};
543
+ }
544
+ const grepArgs = [
545
+ grepOptions.patternSyntax?.basicRegExp
66
546
  ? '--basic-regexp'
67
- : options.patternSyntax?.extendedRegExp
547
+ : grepOptions.patternSyntax?.extendedRegExp
68
548
  ? '--extended-regexp'
69
- : options.patternSyntax?.fixedStrings
549
+ : grepOptions.patternSyntax?.fixedStrings
70
550
  ? '--fixed-strings'
71
551
  : '',
72
- options.ignoreCase ? '--ignore-case' : '',
73
- options.invertMatch && !options.output?.filesOnly ? '--invert-match' : '',
74
- options.matchType?.wordRegExp
552
+ grepOptions.ignoreCase ? '--ignore-case' : '',
553
+ grepOptions.invertMatch && !grepOptions.output?.filesOnly ? '--invert-match' : '',
554
+ grepOptions.matchType?.wordRegExp
75
555
  ? '--word-regexp'
76
- : options.matchType?.lineRegExp
556
+ : grepOptions.matchType?.lineRegExp
77
557
  ? '--line-regexp'
78
558
  : '',
79
- options.output?.countOnly
559
+ grepOptions.output?.countOnly
80
560
  ? '--count'
81
- : options.output?.filesOnly
82
- ? options.invertMatch
561
+ : grepOptions.output?.filesOnly
562
+ ? grepOptions.invertMatch
83
563
  ? '--files-without-match'
84
564
  : '--files-with-matches'
85
565
  : '',
86
566
  '--color=never',
87
- options.maxCount ? `--max-count=${options.maxCount}` : '',
567
+ grepOptions.maxCount == undefined ? '' : `--max-count=${grepOptions.maxCount}`,
88
568
  '--no-messages',
569
+ '--devices=skip',
89
570
  '--with-filename',
90
571
  '--null',
91
- ...(options.excludePatterns?.length
92
- ? options.excludePatterns.map((excludePattern) => `--exclude="${escape(excludePattern)}"`)
572
+ ...(grepOptionArrays.excludePatterns.length
573
+ ? grepOptionArrays.excludePatterns.map((excludePattern) => `--exclude=${excludePattern}`)
93
574
  : []),
94
- recursiveFlag(options),
95
- ...(options.excludeDirs?.length
96
- ? options.excludeDirs.map((excludeDir) => `--exclude-dir="${escape(excludeDir)}"`)
575
+ recursiveFlag(grepOptions),
576
+ ...(grepOptionArrays.excludeDirs.length
577
+ ? grepOptionArrays.excludeDirs.map((excludeDir) => `--exclude-dir=${excludeDir}`)
97
578
  : []),
98
- ...(options.includeFiles?.length
99
- ? options.includeFiles.map((includeFile) => `--include="${escape(includeFile)}"`)
579
+ ...(grepOptionArrays.includeFiles.length
580
+ ? grepOptionArrays.includeFiles.map((includeFile) => `--include=${includeFile}`)
100
581
  : []),
101
- options.binary ? '--binary' : '',
102
- ...searchPatterns.map((searchPattern) => `-e "${searchPattern}"`),
582
+ grepOptions.binary ? '--binary' : '',
583
+ ...searchPatterns.flatMap((searchPattern) => [
584
+ '-e',
585
+ searchPattern,
586
+ ]),
587
+ '--',
103
588
  ...searchParts,
104
- ]
105
- .filter(check.isTruthy)
106
- .join(' ');
107
- if (options.printCommand) {
108
- log.faint(`> ${fullCommand}`);
589
+ ].filter(check.isTruthy);
590
+ if (grepOptions.printCommand) {
591
+ log.faint(`> ${formatGrepCommand(grepArgs)}`);
109
592
  }
110
- const result = await runShellCommand(fullCommand, {
111
- cwd: options.cwd,
593
+ const result = await runGrepCommand({
594
+ args: grepArgs,
595
+ cwd,
112
596
  });
113
- const trimmedOutput = result.stdout.trim();
114
- if (result.exitCode === 1 || !trimmedOutput) {
597
+ if (didGrepFail(result) || result.exitCode === 1 || !result.stdout) {
115
598
  /** No matches. */
116
599
  return {};
117
600
  }
118
- else if (options.output?.countOnly) {
119
- return arrayToObject(trimmedOutput.split('\n'), (entry) => {
120
- /** Ignore empty strings. */
121
- /* node:coverage ignore next 3 */
122
- if (!entry) {
123
- return undefined;
124
- }
125
- /**
126
- * GNU `grep` (Linux) separates the file name from its count with a null byte when
127
- * `--null` is set, while BSD `grep` (macOS) uses a colon. Accept either.
128
- */
129
- const [, fileName, countString,] = safeMatch(entry, /^(.+)[\0:](\d+)$/);
130
- assert.isDefined(fileName, `Failed parse grep file name from: '${entry}'`);
131
- const count = Number(countString);
132
- assert.isNumber(count, `Failed to parse grep number from: '${entry}'`);
133
- if (!count) {
134
- return undefined;
135
- }
136
- return {
137
- key: fileName,
138
- value: count,
139
- };
140
- }, {
141
- useRequired: true,
142
- });
601
+ else if (grepOptions.output?.countOnly) {
602
+ /* node:coverage ignore next 4 */
603
+ const parsedCountOutput = isOperatingSystem(OperatingSystem.Mac) && !result.stdout.includes('\0')
604
+ ? undefined
605
+ : tryParseGrepCountOutput(result.stdout);
606
+ /* node:coverage ignore next 3 */
607
+ if (parsedCountOutput) {
608
+ return parsedCountOutput;
609
+ }
610
+ return (await runGrepCountFallback({
611
+ cwd,
612
+ grepArgs,
613
+ }));
143
614
  }
144
- else if (options.output?.filesOnly) {
145
- return arrayToObject(trimmedOutput.split(/[\0\n]/), (entry) => {
146
- /** Ignore empty strings. */
147
- if (!entry) {
148
- return undefined;
149
- }
150
- return {
151
- key: entry,
152
- value: [],
153
- };
154
- }, {
155
- useRequired: true,
156
- });
615
+ else if (grepOptions.output?.filesOnly) {
616
+ return parseGrepFilesOnlyOutput(result.stdout);
157
617
  }
158
618
  else {
159
- const outputLines = trimmedOutput.split(/[\0\n]/);
160
- const fileMatches = {};
161
- outputLines.forEach((line, index) => {
162
- if (!(index % 2)) {
163
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
164
- getOrSet(fileMatches, line, () => []).push(outputLines[index + 1]);
165
- }
166
- });
167
- return fileMatches;
619
+ return parseGrepNormalOutput(result.stdout);
168
620
  }
169
621
  }