@augment-vir/node 31.45.0 → 31.46.1

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.
@@ -0,0 +1,183 @@
1
+ import { type PartialWithUndefined } from '@augment-vir/common';
2
+ import { type IsEqual, type RequireExactlyOne } from 'type-fest';
3
+ /**
4
+ * Optional options for {@link grep}.
5
+ *
6
+ * @category Internal
7
+ * @category Package : @augment-vir/node
8
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
9
+ */
10
+ export type GrepOptions<CountOnly extends boolean = false> = PartialWithUndefined<{
11
+ patternSyntax: RequireExactlyOne<{
12
+ /**
13
+ * -E, --extended-regexp: Interpret PATTERNS as extended regular expressions (EREs, see
14
+ * below).
15
+ */
16
+ extendedRegExp: true;
17
+ /** -F, --fixed-strings: Interpret PATTERNS as fixed strings, not regular expressions. */
18
+ fixedStrings: true;
19
+ /**
20
+ * -G, --basic-regexp: Interpret PATTERNS as basic regular expressions (BREs, see below).
21
+ * This is the default.
22
+ */
23
+ basicRegExp: true;
24
+ }>;
25
+ /**
26
+ * If set to true, sets: -i, --ignore-case: Ignore case distinctions in patterns and input data,
27
+ * so that characters that differ only in case match each other.
28
+ */
29
+ ignoreCase: boolean;
30
+ /** -v, --invert-match: Invert the sense of matching, to select non-matching lines. */
31
+ invertMatch: boolean;
32
+ matchType: RequireExactlyOne<{
33
+ /**
34
+ * -w, --word-regexp: Select only those lines containing matches that form whole words. The
35
+ * test is that the matching substring must either be at the beginning of the line, or
36
+ * preceded by a non-word constituent character. Similarly, it must be either at the end of
37
+ * the line or followed by a non-word constituent character. Word-constituent characters are
38
+ * letters, digits, and the underscore. This option has no effect if -x is also specified.
39
+ */
40
+ wordRegExp: true;
41
+ /**
42
+ * -x, --line-regexp: Select only those matches that exactly match the whole line. For a
43
+ * regular expression pattern, this is like parenthesizing the pattern and then surrounding
44
+ * it with ^ and $.
45
+ */
46
+ lineRegExp: true;
47
+ }>;
48
+ output: RequireExactlyOne<{
49
+ /**
50
+ * -c, --count: Suppress normal output; instead print a count of matching lines for each
51
+ * input file. With the -v, --invert-match option (see above), count non-matching lines.
52
+ */
53
+ countOnly: CountOnly;
54
+ /**
55
+ * -l, --files-with-matches: Suppress normal output; instead print the name of each input
56
+ * file from which output would normally have been printed. Scanning each input file stops
57
+ * upon first match.
58
+ */
59
+ filesOnly: true;
60
+ }>;
61
+ /**
62
+ * --exclude=GLOB: Skip any command-line file with a name suffix that matches the pattern GLOB,
63
+ * using wildcard matching; a name suffix is either the whole name, or a trailing part that
64
+ * starts with a non-slash character immediately after a slash (/) in the name. When searching
65
+ * recursively, skip any subfile whose base name matches GLOB; the base name is the part after
66
+ * the last slash. A pattern can use *, ?, and [...] as wildcards, and \ to quote a wildcard or
67
+ * backslash character literally.
68
+ */
69
+ excludePatterns: string[];
70
+ /**
71
+ * -m NUM, --max-count=NUM: Stop reading a file after NUM matching lines. If NUM is zero, grep
72
+ * stops right away without reading input. A NUM of -1 is treated as infinity and grep does not
73
+ * stop; this is the default. If the input is standard input from a regular file, and NUM
74
+ * matching lines are output, grep ensures that the standard input is positioned to just after
75
+ * the last matching line before exiting, regardless of the presence of trailing context lines.
76
+ * This enables a calling process to resume a search. When grep stops after NUM matching lines,
77
+ * it outputs any trailing context lines. When the -c or --count option is also used, grep does
78
+ * not output a count greater than NUM. When the -v or --invert-match option is also used, grep
79
+ * stops after outputting NUM non-matching lines.
80
+ */
81
+ maxCount: number;
82
+ /**
83
+ * -r, --recursive: Read all files under each directory, recursively, following symbolic links
84
+ * only if they are on the command line. Note that if no file operand is given, grep searches
85
+ * the working directory. This is equivalent to the -d recurse option.
86
+ */
87
+ recursive: boolean;
88
+ /**
89
+ * If `true`, sets `--dereference-recursive` instead of `--recursive` when searching
90
+ * recursively.
91
+ *
92
+ * -R, --dereference-recursive: Read all files under each directory, recursively. Follow all
93
+ * symbolic links, unlike -r.
94
+ */
95
+ followSymLinks: boolean;
96
+ /**
97
+ * -U, --binary: Treat the file(s) as binary. By default, under MS-DOS and MS-Windows, grep
98
+ * guesses whether a file is text or binary as described for the --binary-files option. If grep
99
+ * decides the file is a text file, it strips the CR characters from the original file contents
100
+ * (to make regular expressions with ^ and $ work correctly). Specifying -U overrules this
101
+ * guesswork, causing all files to be read and passed to the matching mechanism verbatim; if the
102
+ * file is a text file with CR/LF pairs at the end of each line, this will cause some regular
103
+ * expressions to fail. This option has no effect on platforms other than MS-DOS and MS-
104
+ * Windows.
105
+ */
106
+ binary: boolean;
107
+ /**
108
+ * --exclude-dir=GLOB: Skip any command-line directory with a name suffix that matches the
109
+ * pattern GLOB. When searching recursively, skip any subdirectory whose base name matches GLOB.
110
+ * Ignore any redundant trailing slashes in GLOB.
111
+ */
112
+ excludeDirs: string[];
113
+ /**
114
+ * --include=GLOB: Search only files whose base name matches GLOB (using wildcard matching as
115
+ * described under --exclude). If contradictory --include and --exclude options are given, the
116
+ * last matching one wins. If no --include or --exclude options match, a file is included unless
117
+ * the first such option is --include.
118
+ */
119
+ includeFiles: string[];
120
+ /** Debugging option: if set to `true`, the grep CLI command will be printed before execution. */
121
+ printCommand: boolean;
122
+ /** The directory where the grep command where be run within. */
123
+ cwd: string;
124
+ }>;
125
+ /**
126
+ * Search location options for {@link grep}.
127
+ *
128
+ * @category Internal
129
+ * @category Package : @augment-vir/node
130
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
131
+ */
132
+ export type GrepSearchLocation = RequireExactlyOne<{
133
+ /** Search within multiple files. */
134
+ files: string[];
135
+ /**
136
+ * Search within multiple directories. Set `recursive` to `true` in options to search the
137
+ * directory recursively.
138
+ */
139
+ dirs: string[];
140
+ /**
141
+ * Search within a single directory. Set `recursive` to `true` in options to search the
142
+ * directory recursively.
143
+ */
144
+ dir: string;
145
+ /** Search within a single file. */
146
+ file: string;
147
+ }>;
148
+ /**
149
+ * Search pattern options for {@link grep}.
150
+ *
151
+ * @category Internal
152
+ * @category Package : @augment-vir/node
153
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
154
+ */
155
+ export type GrepSearchPattern = RequireExactlyOne<{
156
+ pattern: string;
157
+ patterns: string[];
158
+ }>;
159
+ /**
160
+ * Output of {@link grep}. Each key is an absolute file path. Values are array of matches lines for
161
+ * that file.
162
+ *
163
+ * @category Internal
164
+ * @category Package : @augment-vir/node
165
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
166
+ */
167
+ export type GrepMatches<CountOnly extends boolean = false> = IsEqual<CountOnly, true> extends true ? {
168
+ [FileName in string]: number;
169
+ } : IsEqual<CountOnly, false> extends true ? {
170
+ [FileName in string]: string[];
171
+ } : {
172
+ [FileName in string]: number;
173
+ } | {
174
+ [FileName in string]: string[];
175
+ };
176
+ /**
177
+ * Run `grep`, matching patterns to specific lines in files or directories.
178
+ *
179
+ * @category Node : File
180
+ * @category Package : @augment-vir/node
181
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
182
+ */
183
+ export declare function grep<const CountOnly extends boolean = false>(grepSearchPattern: Readonly<GrepSearchPattern>, grepSearchLocation: Readonly<GrepSearchLocation>, options?: Readonly<GrepOptions<CountOnly>>): Promise<GrepMatches<CountOnly>>;
@@ -0,0 +1,153 @@
1
+ import { assert, check } from '@augment-vir/assert';
2
+ import { arrayToObject, getOrSet, log, safeMatch, } from '@augment-vir/common';
3
+ import { join } from 'node:path';
4
+ import { runShellCommand } from '../terminal/shell.js';
5
+ function escape(input) {
6
+ return input.replaceAll('"', String.raw `\"`).replaceAll('\n', '');
7
+ }
8
+ /**
9
+ * Run `grep`, matching patterns to specific lines in files or directories.
10
+ *
11
+ * @category Node : File
12
+ * @category Package : @augment-vir/node
13
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
14
+ */
15
+ export async function grep(grepSearchPattern, grepSearchLocation, options = {}) {
16
+ const searchPatterns = (grepSearchPattern.patterns || [grepSearchPattern.pattern]).filter(check.isTruthy);
17
+ if (!searchPatterns.length) {
18
+ return {};
19
+ }
20
+ const searchLocation = grepSearchLocation.files
21
+ ? {
22
+ files: grepSearchLocation.files,
23
+ }
24
+ : grepSearchLocation.file
25
+ ? {
26
+ files: [grepSearchLocation.file],
27
+ }
28
+ : grepSearchLocation.dirs
29
+ ? {
30
+ dirs: grepSearchLocation.dirs,
31
+ }
32
+ : grepSearchLocation.dir
33
+ ? {
34
+ dirs: [grepSearchLocation.dir],
35
+ }
36
+ : undefined;
37
+ if (!searchLocation ||
38
+ (searchLocation.dirs && !searchLocation.dirs.length) ||
39
+ (searchLocation.files && !searchLocation.files.length)) {
40
+ return {};
41
+ }
42
+ const searchParts = searchLocation.dirs
43
+ ? options.recursive
44
+ ? searchLocation.dirs
45
+ : searchLocation.dirs.map((dir) => join(dir, '*'))
46
+ : searchLocation.files;
47
+ const fullCommand = [
48
+ 'grep',
49
+ options.patternSyntax?.basicRegExp
50
+ ? '--basic-regexp'
51
+ : options.patternSyntax?.extendedRegExp
52
+ ? '--extended-regexp'
53
+ : options.patternSyntax?.fixedStrings
54
+ ? '--fixed-strings'
55
+ : '',
56
+ options.ignoreCase ? '--ignore-case' : '',
57
+ options.invertMatch && !options.output?.filesOnly ? '--invert-match' : '',
58
+ options.matchType?.wordRegExp
59
+ ? '--word-regexp'
60
+ : options.matchType?.lineRegExp
61
+ ? '--line-regexp'
62
+ : '',
63
+ options.output?.countOnly
64
+ ? '--count'
65
+ : options.output?.filesOnly
66
+ ? options.invertMatch
67
+ ? '--files-without-match'
68
+ : '--files-with-matches'
69
+ : '',
70
+ '--color=never',
71
+ options.maxCount ? `--max-count=${options.maxCount}` : '',
72
+ '--no-messages',
73
+ '--with-filename',
74
+ '--null',
75
+ ...(options.excludePatterns?.length
76
+ ? options.excludePatterns.map((excludePattern) => `--exclude="${escape(excludePattern)}"`)
77
+ : []),
78
+ options.recursive
79
+ ? options.followSymLinks
80
+ ? '-RS'
81
+ : '--recursive'
82
+ : '',
83
+ ...(options.excludeDirs?.length
84
+ ? options.excludeDirs.map((excludeDir) => `--exclude-dir="${escape(excludeDir)}"`)
85
+ : []),
86
+ ...(options.includeFiles?.length
87
+ ? options.includeFiles.map((includeFile) => `--include="${escape(includeFile)}"`)
88
+ : []),
89
+ options.binary ? '--binary' : '',
90
+ ...searchPatterns.map((searchPattern) => `-e "${searchPattern}"`),
91
+ ...searchParts,
92
+ ]
93
+ .filter(check.isTruthy)
94
+ .join(' ');
95
+ if (options.printCommand) {
96
+ log.faint(`> ${fullCommand}`);
97
+ }
98
+ const result = await runShellCommand(fullCommand, {
99
+ cwd: options.cwd,
100
+ });
101
+ const trimmedOutput = result.stdout.trim();
102
+ if (result.exitCode === 1 || !trimmedOutput) {
103
+ /** No matches. */
104
+ return {};
105
+ }
106
+ else if (options.output?.countOnly) {
107
+ return arrayToObject(trimmedOutput.split(/[\0\n]/), (entry) => {
108
+ /** Ignore empty strings. */
109
+ /* node:coverage ignore next 3 */
110
+ if (!entry) {
111
+ return undefined;
112
+ }
113
+ const [, fileName, countString,] = safeMatch(entry, /(^.+):(\d+)$/);
114
+ assert.isDefined(fileName, `Failed parse grep file name from: '${entry}'`);
115
+ const count = Number(countString);
116
+ assert.isNumber(count, `Failed to parse grep number from: '${entry}'`);
117
+ if (!count) {
118
+ return undefined;
119
+ }
120
+ return {
121
+ key: fileName,
122
+ value: count,
123
+ };
124
+ }, {
125
+ useRequired: true,
126
+ });
127
+ }
128
+ else if (options.output?.filesOnly) {
129
+ return arrayToObject(trimmedOutput.split(/[\0\n]/), (entry) => {
130
+ /** Ignore empty strings. */
131
+ if (!entry) {
132
+ return undefined;
133
+ }
134
+ return {
135
+ key: entry,
136
+ value: [],
137
+ };
138
+ }, {
139
+ useRequired: true,
140
+ });
141
+ }
142
+ else {
143
+ const outputLines = trimmedOutput.split(/[\0\n]/);
144
+ const fileMatches = {};
145
+ outputLines.forEach((line, index) => {
146
+ if (!(index % 2)) {
147
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
148
+ getOrSet(fileMatches, line, () => []).push(outputLines[index + 1]);
149
+ }
150
+ });
151
+ return fileMatches;
152
+ }
153
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './augments/fs/dir-contents.js';
2
2
  export * from './augments/fs/download.js';
3
+ export * from './augments/fs/grep.js';
3
4
  export * from './augments/fs/json.js';
4
5
  export * from './augments/fs/read-dir.js';
5
6
  export * from './augments/fs/read-file.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './augments/fs/dir-contents.js';
2
2
  export * from './augments/fs/download.js';
3
+ export * from './augments/fs/grep.js';
3
4
  export * from './augments/fs/json.js';
4
5
  export * from './augments/fs/read-dir.js';
5
6
  export * from './augments/fs/read-file.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@augment-vir/node",
3
- "version": "31.45.0",
3
+ "version": "31.46.1",
4
4
  "description": "A collection of augments, helpers types, functions, and classes only for Node.js (backend) JavaScript environments.",
5
5
  "keywords": [
6
6
  "augment",
@@ -38,8 +38,8 @@
38
38
  "test:update": "npm test"
39
39
  },
40
40
  "dependencies": {
41
- "@augment-vir/assert": "^31.45.0",
42
- "@augment-vir/common": "^31.45.0",
41
+ "@augment-vir/assert": "^31.46.1",
42
+ "@augment-vir/common": "^31.46.1",
43
43
  "@date-vir/duration": "^8.0.0",
44
44
  "ansi-styles": "^6.2.3",
45
45
  "sanitize-filename": "^1.6.3",
@@ -49,7 +49,7 @@
49
49
  "typed-event-target": "^4.1.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@augment-vir/test": "^31.45.0",
52
+ "@augment-vir/test": "^31.46.1",
53
53
  "@types/node": "^24.9.1",
54
54
  "@web/dev-server-esbuild": "^1.0.4",
55
55
  "@web/test-runner": "^0.20.2",
@@ -0,0 +1,379 @@
1
+ import {assert, check} from '@augment-vir/assert';
2
+ import {
3
+ arrayToObject,
4
+ getOrSet,
5
+ log,
6
+ safeMatch,
7
+ type PartialWithUndefined,
8
+ type SelectFrom,
9
+ } from '@augment-vir/common';
10
+ import {join} from 'node:path';
11
+ import {type IsEqual, type RequireExactlyOne} from 'type-fest';
12
+ import {runShellCommand} from '../terminal/shell.js';
13
+
14
+ /**
15
+ * Optional options for {@link grep}.
16
+ *
17
+ * @category Internal
18
+ * @category Package : @augment-vir/node
19
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
20
+ */
21
+ export type GrepOptions<CountOnly extends boolean = false> = PartialWithUndefined<{
22
+ patternSyntax: RequireExactlyOne<{
23
+ /**
24
+ * -E, --extended-regexp: Interpret PATTERNS as extended regular expressions (EREs, see
25
+ * below).
26
+ */
27
+ extendedRegExp: true;
28
+ /** -F, --fixed-strings: Interpret PATTERNS as fixed strings, not regular expressions. */
29
+ fixedStrings: true;
30
+ /**
31
+ * -G, --basic-regexp: Interpret PATTERNS as basic regular expressions (BREs, see below).
32
+ * This is the default.
33
+ */
34
+ basicRegExp: true;
35
+ }>;
36
+ /**
37
+ * If set to true, sets: -i, --ignore-case: Ignore case distinctions in patterns and input data,
38
+ * so that characters that differ only in case match each other.
39
+ */
40
+ ignoreCase: boolean;
41
+ /** -v, --invert-match: Invert the sense of matching, to select non-matching lines. */
42
+ invertMatch: boolean;
43
+ matchType: RequireExactlyOne<{
44
+ /**
45
+ * -w, --word-regexp: Select only those lines containing matches that form whole words. The
46
+ * test is that the matching substring must either be at the beginning of the line, or
47
+ * preceded by a non-word constituent character. Similarly, it must be either at the end of
48
+ * the line or followed by a non-word constituent character. Word-constituent characters are
49
+ * letters, digits, and the underscore. This option has no effect if -x is also specified.
50
+ */
51
+ wordRegExp: true;
52
+ /**
53
+ * -x, --line-regexp: Select only those matches that exactly match the whole line. For a
54
+ * regular expression pattern, this is like parenthesizing the pattern and then surrounding
55
+ * it with ^ and $.
56
+ */
57
+ lineRegExp: true;
58
+ }>;
59
+
60
+ output: RequireExactlyOne<{
61
+ /**
62
+ * -c, --count: Suppress normal output; instead print a count of matching lines for each
63
+ * input file. With the -v, --invert-match option (see above), count non-matching lines.
64
+ */
65
+ countOnly: CountOnly;
66
+ /**
67
+ * -l, --files-with-matches: Suppress normal output; instead print the name of each input
68
+ * file from which output would normally have been printed. Scanning each input file stops
69
+ * upon first match.
70
+ */
71
+ filesOnly: true;
72
+ }>;
73
+
74
+ /**
75
+ * --exclude=GLOB: Skip any command-line file with a name suffix that matches the pattern GLOB,
76
+ * using wildcard matching; a name suffix is either the whole name, or a trailing part that
77
+ * starts with a non-slash character immediately after a slash (/) in the name. When searching
78
+ * recursively, skip any subfile whose base name matches GLOB; the base name is the part after
79
+ * the last slash. A pattern can use *, ?, and [...] as wildcards, and \ to quote a wildcard or
80
+ * backslash character literally.
81
+ */
82
+ excludePatterns: string[];
83
+
84
+ /**
85
+ * -m NUM, --max-count=NUM: Stop reading a file after NUM matching lines. If NUM is zero, grep
86
+ * stops right away without reading input. A NUM of -1 is treated as infinity and grep does not
87
+ * stop; this is the default. If the input is standard input from a regular file, and NUM
88
+ * matching lines are output, grep ensures that the standard input is positioned to just after
89
+ * the last matching line before exiting, regardless of the presence of trailing context lines.
90
+ * This enables a calling process to resume a search. When grep stops after NUM matching lines,
91
+ * it outputs any trailing context lines. When the -c or --count option is also used, grep does
92
+ * not output a count greater than NUM. When the -v or --invert-match option is also used, grep
93
+ * stops after outputting NUM non-matching lines.
94
+ */
95
+ maxCount: number;
96
+ /**
97
+ * -r, --recursive: Read all files under each directory, recursively, following symbolic links
98
+ * only if they are on the command line. Note that if no file operand is given, grep searches
99
+ * the working directory. This is equivalent to the -d recurse option.
100
+ */
101
+ recursive: boolean;
102
+ /**
103
+ * If `true`, sets `--dereference-recursive` instead of `--recursive` when searching
104
+ * recursively.
105
+ *
106
+ * -R, --dereference-recursive: Read all files under each directory, recursively. Follow all
107
+ * symbolic links, unlike -r.
108
+ */
109
+ followSymLinks: boolean;
110
+ /**
111
+ * -U, --binary: Treat the file(s) as binary. By default, under MS-DOS and MS-Windows, grep
112
+ * guesses whether a file is text or binary as described for the --binary-files option. If grep
113
+ * decides the file is a text file, it strips the CR characters from the original file contents
114
+ * (to make regular expressions with ^ and $ work correctly). Specifying -U overrules this
115
+ * guesswork, causing all files to be read and passed to the matching mechanism verbatim; if the
116
+ * file is a text file with CR/LF pairs at the end of each line, this will cause some regular
117
+ * expressions to fail. This option has no effect on platforms other than MS-DOS and MS-
118
+ * Windows.
119
+ */
120
+ binary: boolean;
121
+ /**
122
+ * --exclude-dir=GLOB: Skip any command-line directory with a name suffix that matches the
123
+ * pattern GLOB. When searching recursively, skip any subdirectory whose base name matches GLOB.
124
+ * Ignore any redundant trailing slashes in GLOB.
125
+ */
126
+ excludeDirs: string[];
127
+ /**
128
+ * --include=GLOB: Search only files whose base name matches GLOB (using wildcard matching as
129
+ * described under --exclude). If contradictory --include and --exclude options are given, the
130
+ * last matching one wins. If no --include or --exclude options match, a file is included unless
131
+ * the first such option is --include.
132
+ */
133
+ includeFiles: string[];
134
+ /** Debugging option: if set to `true`, the grep CLI command will be printed before execution. */
135
+ printCommand: boolean;
136
+ /** The directory where the grep command where be run within. */
137
+ cwd: string;
138
+ }>;
139
+
140
+ /**
141
+ * Search location options for {@link grep}.
142
+ *
143
+ * @category Internal
144
+ * @category Package : @augment-vir/node
145
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
146
+ */
147
+ export type GrepSearchLocation = RequireExactlyOne<{
148
+ /** Search within multiple files. */
149
+ files: string[];
150
+ /**
151
+ * Search within multiple directories. Set `recursive` to `true` in options to search the
152
+ * directory recursively.
153
+ */
154
+ dirs: string[];
155
+
156
+ /**
157
+ * Search within a single directory. Set `recursive` to `true` in options to search the
158
+ * directory recursively.
159
+ */
160
+ dir: string;
161
+ /** Search within a single file. */
162
+ file: string;
163
+ }>;
164
+
165
+ /**
166
+ * Search pattern options for {@link grep}.
167
+ *
168
+ * @category Internal
169
+ * @category Package : @augment-vir/node
170
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
171
+ */
172
+ export type GrepSearchPattern = RequireExactlyOne<{
173
+ pattern: string;
174
+ patterns: string[];
175
+ }>;
176
+
177
+ function escape(input: string) {
178
+ return input.replaceAll('"', String.raw`\"`).replaceAll('\n', '');
179
+ }
180
+
181
+ /**
182
+ * Output of {@link grep}. Each key is an absolute file path. Values are array of matches lines for
183
+ * that file.
184
+ *
185
+ * @category Internal
186
+ * @category Package : @augment-vir/node
187
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
188
+ */
189
+ export type GrepMatches<CountOnly extends boolean = false> =
190
+ IsEqual<CountOnly, true> extends true
191
+ ? {[FileName in string]: number}
192
+ : IsEqual<CountOnly, false> extends true
193
+ ? {[FileName in string]: string[]}
194
+ : {[FileName in string]: number} | {[FileName in string]: string[]};
195
+
196
+ /**
197
+ * Run `grep`, matching patterns to specific lines in files or directories.
198
+ *
199
+ * @category Node : File
200
+ * @category Package : @augment-vir/node
201
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
202
+ */
203
+ export async function grep<const CountOnly extends boolean = false>(
204
+ grepSearchPattern: Readonly<GrepSearchPattern>,
205
+ grepSearchLocation: Readonly<GrepSearchLocation>,
206
+ options: Readonly<GrepOptions<CountOnly>> = {},
207
+ ): Promise<GrepMatches<CountOnly>> {
208
+ const searchPatterns: string[] = (
209
+ grepSearchPattern.patterns || [grepSearchPattern.pattern]
210
+ ).filter(check.isTruthy);
211
+
212
+ if (!searchPatterns.length) {
213
+ return {};
214
+ }
215
+
216
+ const searchLocation: SelectFrom<GrepSearchLocation, {files: true; dirs: true}> | undefined =
217
+ grepSearchLocation.files
218
+ ? {
219
+ files: grepSearchLocation.files,
220
+ }
221
+ : grepSearchLocation.file
222
+ ? {
223
+ files: [grepSearchLocation.file],
224
+ }
225
+ : grepSearchLocation.dirs
226
+ ? {
227
+ dirs: grepSearchLocation.dirs,
228
+ }
229
+ : grepSearchLocation.dir
230
+ ? {
231
+ dirs: [grepSearchLocation.dir],
232
+ }
233
+ : undefined;
234
+
235
+ if (
236
+ !searchLocation ||
237
+ (searchLocation.dirs && !searchLocation.dirs.length) ||
238
+ (searchLocation.files && !searchLocation.files.length)
239
+ ) {
240
+ return {};
241
+ }
242
+
243
+ const searchParts = searchLocation.dirs
244
+ ? options.recursive
245
+ ? searchLocation.dirs
246
+ : searchLocation.dirs.map((dir) => join(dir, '*'))
247
+ : searchLocation.files;
248
+
249
+ const fullCommand = [
250
+ 'grep',
251
+ options.patternSyntax?.basicRegExp
252
+ ? '--basic-regexp'
253
+ : options.patternSyntax?.extendedRegExp
254
+ ? '--extended-regexp'
255
+ : options.patternSyntax?.fixedStrings
256
+ ? '--fixed-strings'
257
+ : '',
258
+ options.ignoreCase ? '--ignore-case' : '',
259
+ options.invertMatch && !options.output?.filesOnly ? '--invert-match' : '',
260
+ options.matchType?.wordRegExp
261
+ ? '--word-regexp'
262
+ : options.matchType?.lineRegExp
263
+ ? '--line-regexp'
264
+ : '',
265
+ options.output?.countOnly
266
+ ? '--count'
267
+ : options.output?.filesOnly
268
+ ? options.invertMatch
269
+ ? '--files-without-match'
270
+ : '--files-with-matches'
271
+ : '',
272
+ '--color=never',
273
+ options.maxCount ? `--max-count=${options.maxCount}` : '',
274
+ '--no-messages',
275
+ '--with-filename',
276
+ '--null',
277
+ ...(options.excludePatterns?.length
278
+ ? options.excludePatterns.map(
279
+ (excludePattern) => `--exclude="${escape(excludePattern)}"`,
280
+ )
281
+ : []),
282
+ options.recursive
283
+ ? options.followSymLinks
284
+ ? '-RS'
285
+ : '--recursive'
286
+ : '',
287
+ ...(options.excludeDirs?.length
288
+ ? options.excludeDirs.map((excludeDir) => `--exclude-dir="${escape(excludeDir)}"`)
289
+ : []),
290
+ ...(options.includeFiles?.length
291
+ ? options.includeFiles.map((includeFile) => `--include="${escape(includeFile)}"`)
292
+ : []),
293
+ options.binary ? '--binary' : '',
294
+ ...searchPatterns.map((searchPattern) => `-e "${searchPattern}"`),
295
+ ...searchParts,
296
+ ]
297
+ .filter(check.isTruthy)
298
+ .join(' ');
299
+
300
+ if (options.printCommand) {
301
+ log.faint(`> ${fullCommand}`);
302
+ }
303
+
304
+ const result = await runShellCommand(fullCommand, {
305
+ cwd: options.cwd,
306
+ });
307
+
308
+ const trimmedOutput = result.stdout.trim();
309
+
310
+ if (result.exitCode === 1 || !trimmedOutput) {
311
+ /** No matches. */
312
+ return {};
313
+ } else if (options.output?.countOnly) {
314
+ return arrayToObject(
315
+ trimmedOutput.split(/[\0\n]/),
316
+ (entry) => {
317
+ /** Ignore empty strings. */
318
+ /* node:coverage ignore next 3 */
319
+ if (!entry) {
320
+ return undefined;
321
+ }
322
+
323
+ const [
324
+ ,
325
+ fileName,
326
+ countString,
327
+ ] = safeMatch(entry, /(^.+):(\d+)$/);
328
+
329
+ assert.isDefined(fileName, `Failed parse grep file name from: '${entry}'`);
330
+
331
+ const count = Number(countString);
332
+
333
+ assert.isNumber(count, `Failed to parse grep number from: '${entry}'`);
334
+ if (!count) {
335
+ return undefined;
336
+ }
337
+
338
+ return {
339
+ key: fileName,
340
+ value: count,
341
+ };
342
+ },
343
+ {
344
+ useRequired: true,
345
+ },
346
+ ) satisfies Record<string, number> as GrepMatches<CountOnly>;
347
+ } else if (options.output?.filesOnly) {
348
+ return arrayToObject(
349
+ trimmedOutput.split(/[\0\n]/),
350
+ (entry) => {
351
+ /** Ignore empty strings. */
352
+ if (!entry) {
353
+ return undefined;
354
+ }
355
+
356
+ return {
357
+ key: entry,
358
+ value: [],
359
+ };
360
+ },
361
+ {
362
+ useRequired: true,
363
+ },
364
+ ) satisfies Record<string, string[]> as GrepMatches<CountOnly>;
365
+ } else {
366
+ const outputLines = trimmedOutput.split(/[\0\n]/);
367
+
368
+ const fileMatches: Record<string, string[]> = {};
369
+
370
+ outputLines.forEach((line, index) => {
371
+ if (!(index % 2)) {
372
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
373
+ getOrSet(fileMatches, line, () => []).push(outputLines[index + 1]!);
374
+ }
375
+ });
376
+
377
+ return fileMatches;
378
+ }
379
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './augments/fs/dir-contents.js';
2
2
  export * from './augments/fs/download.js';
3
+ export * from './augments/fs/grep.js';
3
4
  export * from './augments/fs/json.js';
4
5
  export * from './augments/fs/read-dir.js';
5
6
  export * from './augments/fs/read-file.js';