@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.
@@ -1,16 +1,10 @@
1
1
  import { assert, check } from '@augment-vir/assert';
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';
2
+ import { arrayToObject, getOrSet, log, safeMatch, } from '@augment-vir/common';
3
+ import { join } from 'node:path';
6
4
  import { isOperatingSystem, OperatingSystem } from '../os/operating-system.js';
7
- const grepBinPath = '/usr/bin/grep';
8
- function shellQuote(input) {
9
- return [
10
- "'",
11
- input.replaceAll("'", String.raw `'\''`),
12
- "'",
13
- ].join('');
5
+ import { runShellCommand } from '../terminal/shell.js';
6
+ function escape(input) {
7
+ return input.replaceAll('"', String.raw `\"`).replaceAll('\n', '');
14
8
  }
15
9
  function recursiveFlag({ recursive, followSymLinks, }) {
16
10
  if (!recursive) {
@@ -27,484 +21,6 @@ function recursiveFlag({ recursive, followSymLinks, }) {
27
21
  /* node:coverage ignore next */
28
22
  return isOperatingSystem(OperatingSystem.Mac) ? '-RS' : '-R';
29
23
  }
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
- }
508
24
  /**
509
25
  * Run `grep`, matching patterns to specific lines in files or directories.
510
26
  *
@@ -513,109 +29,141 @@ function parseGrepNormalOutput(stdout) {
513
29
  * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
514
30
  */
515
31
  export async function grep(grepSearchPattern, grepSearchLocation, options = {}) {
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
- })) {
32
+ const searchPatterns = (grepSearchPattern.patterns || [grepSearchPattern.pattern]).filter(check.isTruthy);
33
+ if (!searchPatterns.length) {
527
34
  return {};
528
35
  }
529
- const searchLocation = createSearchLocation(grepSearchLocation);
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;
530
53
  if (!searchLocation ||
531
54
  (searchLocation.dirs && !searchLocation.dirs.length) ||
532
55
  (searchLocation.files && !searchLocation.files.length)) {
533
56
  return {};
534
57
  }
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
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
546
66
  ? '--basic-regexp'
547
- : grepOptions.patternSyntax?.extendedRegExp
67
+ : options.patternSyntax?.extendedRegExp
548
68
  ? '--extended-regexp'
549
- : grepOptions.patternSyntax?.fixedStrings
69
+ : options.patternSyntax?.fixedStrings
550
70
  ? '--fixed-strings'
551
71
  : '',
552
- grepOptions.ignoreCase ? '--ignore-case' : '',
553
- grepOptions.invertMatch && !grepOptions.output?.filesOnly ? '--invert-match' : '',
554
- grepOptions.matchType?.wordRegExp
72
+ options.ignoreCase ? '--ignore-case' : '',
73
+ options.invertMatch && !options.output?.filesOnly ? '--invert-match' : '',
74
+ options.matchType?.wordRegExp
555
75
  ? '--word-regexp'
556
- : grepOptions.matchType?.lineRegExp
76
+ : options.matchType?.lineRegExp
557
77
  ? '--line-regexp'
558
78
  : '',
559
- grepOptions.output?.countOnly
79
+ options.output?.countOnly
560
80
  ? '--count'
561
- : grepOptions.output?.filesOnly
562
- ? grepOptions.invertMatch
81
+ : options.output?.filesOnly
82
+ ? options.invertMatch
563
83
  ? '--files-without-match'
564
84
  : '--files-with-matches'
565
85
  : '',
566
86
  '--color=never',
567
- grepOptions.maxCount == undefined ? '' : `--max-count=${grepOptions.maxCount}`,
87
+ options.maxCount ? `--max-count=${options.maxCount}` : '',
568
88
  '--no-messages',
569
- '--devices=skip',
570
89
  '--with-filename',
571
90
  '--null',
572
- ...(grepOptionArrays.excludePatterns.length
573
- ? grepOptionArrays.excludePatterns.map((excludePattern) => `--exclude=${excludePattern}`)
91
+ ...(options.excludePatterns?.length
92
+ ? options.excludePatterns.map((excludePattern) => `--exclude="${escape(excludePattern)}"`)
574
93
  : []),
575
- recursiveFlag(grepOptions),
576
- ...(grepOptionArrays.excludeDirs.length
577
- ? grepOptionArrays.excludeDirs.map((excludeDir) => `--exclude-dir=${excludeDir}`)
94
+ recursiveFlag(options),
95
+ ...(options.excludeDirs?.length
96
+ ? options.excludeDirs.map((excludeDir) => `--exclude-dir="${escape(excludeDir)}"`)
578
97
  : []),
579
- ...(grepOptionArrays.includeFiles.length
580
- ? grepOptionArrays.includeFiles.map((includeFile) => `--include=${includeFile}`)
98
+ ...(options.includeFiles?.length
99
+ ? options.includeFiles.map((includeFile) => `--include="${escape(includeFile)}"`)
581
100
  : []),
582
- grepOptions.binary ? '--binary' : '',
583
- ...searchPatterns.flatMap((searchPattern) => [
584
- '-e',
585
- searchPattern,
586
- ]),
587
- '--',
101
+ options.binary ? '--binary' : '',
102
+ ...searchPatterns.map((searchPattern) => `-e "${searchPattern}"`),
588
103
  ...searchParts,
589
- ].filter(check.isTruthy);
590
- if (grepOptions.printCommand) {
591
- log.faint(`> ${formatGrepCommand(grepArgs)}`);
104
+ ]
105
+ .filter(check.isTruthy)
106
+ .join(' ');
107
+ if (options.printCommand) {
108
+ log.faint(`> ${fullCommand}`);
592
109
  }
593
- const result = await runGrepCommand({
594
- args: grepArgs,
595
- cwd,
110
+ const result = await runShellCommand(fullCommand, {
111
+ cwd: options.cwd,
596
112
  });
597
- if (didGrepFail(result) || result.exitCode === 1 || !result.stdout) {
113
+ const trimmedOutput = result.stdout.trim();
114
+ if (result.exitCode === 1 || !trimmedOutput) {
598
115
  /** No matches. */
599
116
  return {};
600
117
  }
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
- }));
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
+ });
614
143
  }
615
- else if (grepOptions.output?.filesOnly) {
616
- return parseGrepFilesOnlyOutput(result.stdout);
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
+ });
617
157
  }
618
158
  else {
619
- return parseGrepNormalOutput(result.stdout);
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;
620
168
  }
621
169
  }