Add -B, -A, and -C options to grep (#1206)

Adds the -B (before context), -A (after context), and -C (before+after context) options to grep. Example usage:

```
grep -B <num> [args...]
grep -A <num> [args...]
```
This commit is contained in:
Alana Gilston 2025-04-19 08:26:51 -07:00 committed by GitHub
parent 3149e09d5f
commit bfd06fa096
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 381 additions and 6 deletions

View File

@ -419,12 +419,18 @@ Available options:
+ `-l`: Print only filenames of matching files.
+ `-i`: Ignore case.
+ `-n`: Print line numbers.
+ `-B <num>`: Show `<num>` lines before each result.
+ `-A <num>`: Show `<num>` lines after each result.
+ `-C <num>`: Show `<num>` lines before and after each result. -B and -A override this option.
Examples:
```javascript
grep('-v', 'GLOBAL_VARIABLE', '*.js');
grep('GLOBAL_VARIABLE', '*.js');
grep('-B', 3, 'GLOBAL_VARIABLE', '*.js');
grep({ '-B': 3 }, 'GLOBAL_VARIABLE', '*.js');
grep({ '-B': 3, '-C': 2 }, 'GLOBAL_VARIABLE', '*.js');
```
Reads input string from given files and returns a

View File

@ -9,6 +9,9 @@ common.register('grep', _grep, {
'l': 'nameOnly',
'i': 'ignoreCase',
'n': 'lineNumber',
'B': 'beforeContext',
'A': 'afterContext',
'C': 'context',
},
});
@ -22,12 +25,18 @@ common.register('grep', _grep, {
//@ + `-l`: Print only filenames of matching files.
//@ + `-i`: Ignore case.
//@ + `-n`: Print line numbers.
//@ + `-B <num>`: Show `<num>` lines before each result.
//@ + `-A <num>`: Show `<num>` lines after each result.
//@ + `-C <num>`: Show `<num>` lines before and after each result. -B and -A override this option.
//@
//@ Examples:
//@
//@ ```javascript
//@ grep('-v', 'GLOBAL_VARIABLE', '*.js');
//@ grep('GLOBAL_VARIABLE', '*.js');
//@ grep('-B', 3, 'GLOBAL_VARIABLE', '*.js');
//@ grep({ '-B': 3 }, 'GLOBAL_VARIABLE', '*.js');
//@ grep({ '-B': 3, '-C': 2 }, 'GLOBAL_VARIABLE', '*.js');
//@ ```
//@
//@ Reads input string from given files and returns a
@ -39,7 +48,41 @@ function _grep(options, regex, files) {
if (!files && !pipe) common.error('no paths given', 2);
files = [].slice.call(arguments, 2);
var idx = 2;
var contextError = ': invalid context length argument';
// If the option has been found but not read, copy value from arguments
if (options.beforeContext === true) {
idx = 3;
options.beforeContext = Number(arguments[1]);
if (options.beforeContext < 0) {
common.error(options.beforeContext + contextError, 2);
}
}
if (options.afterContext === true) {
idx = 3;
options.afterContext = Number(arguments[1]);
if (options.afterContext < 0) {
common.error(options.afterContext + contextError, 2);
}
}
if (options.context === true) {
idx = 3;
options.context = Number(arguments[1]);
if (options.context < 0) {
common.error(options.context + contextError, 2);
}
}
// If before or after not given but context is, update values
if (typeof options.context === 'number') {
if (options.beforeContext === false) {
options.beforeContext = options.context;
}
if (options.afterContext === false) {
options.afterContext = options.context;
}
}
regex = arguments[idx - 1];
files = [].slice.call(arguments, idx);
if (pipe) {
files.unshift('-');
@ -62,16 +105,79 @@ function _grep(options, regex, files) {
}
} else {
var lines = contents.split('\n');
var matches = [];
lines.forEach(function (line, index) {
var matched = line.match(regex);
if ((options.inverse && !matched) || (!options.inverse && matched)) {
var result = line;
if (options.lineNumber) {
result = '' + (index + 1) + ':' + line;
var lineNumber = index + 1;
var result = {};
if (matches.length > 0) {
// If the last result intersects, combine them
var last = matches[matches.length - 1];
var minimumLineNumber = Math.max(
1,
lineNumber - options.beforeContext - 1,
);
if (
last.hasOwnProperty('' + lineNumber) ||
last.hasOwnProperty('' + minimumLineNumber)
) {
result = last;
}
}
result[lineNumber] = {
line,
match: true,
};
if (options.beforeContext > 0) {
// Store the lines with their line numbers to check for overlap
lines
.slice(Math.max(index - options.beforeContext, 0), index)
.forEach(function (v, i, a) {
var lineNum = '' + (index - a.length + i + 1);
if (!result.hasOwnProperty(lineNum)) {
result[lineNum] = { line: v, match: false };
}
});
}
if (options.afterContext > 0) {
// Store the lines with their line numbers to check for overlap
lines
.slice(
index + 1,
Math.min(index + options.afterContext + 1, lines.length - 1),
)
.forEach(function (v, i) {
var lineNum = '' + (index + 1 + i + 1);
if (!result.hasOwnProperty(lineNum)) {
result[lineNum] = { line: v, match: false };
}
});
}
// Only add the result if it's new
if (!matches.includes(result)) {
matches.push(result);
}
grep.push(result);
}
});
// Loop through the matches and add them to the output
Array.prototype.push.apply(
grep,
matches.map(function (result) {
return Object.entries(result)
.map(function (entry) {
var lineNumber = entry[0];
var line = entry[1].line;
var match = entry[1].match;
return options.lineNumber
? lineNumber + (match ? ':' : '-') + line
: line;
})
.join('\n');
}),
);
}
});
@ -79,6 +185,14 @@ function _grep(options, regex, files) {
// We didn't hit the error above, but pattern didn't match
common.error('', { silent: true });
}
return grep.join('\n') + '\n';
var separator = '\n';
if (
typeof options.beforeContext === 'number' ||
typeof options.afterContext === 'number'
) {
separator = '\n--\n';
}
return grep.join(separator) + '\n';
}
module.exports = _grep;

View File

@ -57,6 +57,27 @@ test("multiple files, one doesn't exist, one doesn't match", t => {
t.is(result.code, 2);
});
test('-A option, negative value', t => {
const result = shell.grep('-A', -2, 'test*', 'test/resources/grep/file3');
t.truthy(shell.error());
t.is(result.code, 2);
t.is(result.stderr, 'grep: -2: invalid context length argument');
});
test('-B option, negative value', t => {
const result = shell.grep('-B', -3, 'test*', 'test/resources/grep/file3');
t.truthy(shell.error());
t.is(result.code, 2);
t.is(result.stderr, 'grep: -3: invalid context length argument');
});
test('-C option, negative value', t => {
const result = shell.grep('-C', -1, 'test*', 'test/resources/grep/file3');
t.truthy(shell.error());
t.is(result.code, 2);
t.is(result.stderr, 'grep: -1: invalid context length argument');
});
//
// Valids
//
@ -174,3 +195,222 @@ test('the pattern looks like an option', t => {
t.falsy(shell.error());
t.is(result.toString(), '-v\n-vv\n');
});
//
// Before & after contexts
//
test('-B option', t => {
const result = shell.grep('-B', 3, 'test*', 'test/resources/grep/file3');
t.falsy(shell.error());
t.is(
result.toString(),
'line1\n' +
'line2 test line\n' +
'line3 test line\n' +
'--\n' +
'line7\n' +
'line8\n' +
'line9\n' +
'line10 test line\n' +
'--\n' +
'line12\n' +
'line13\n' +
'line14\n' +
'line15 test line\n',
);
});
test('-B option, -n option', t => {
const result = shell.grep('-nB', 3, 'test*', 'test/resources/grep/file3');
t.falsy(shell.error());
t.is(
result.toString(),
'1-line1\n' +
'2:line2 test line\n' +
'3:line3 test line\n' +
'--\n' +
'7-line7\n' +
'8-line8\n' +
'9-line9\n' +
'10:line10 test line\n' +
'--\n' +
'12-line12\n' +
'13-line13\n' +
'14-line14\n' +
'15:line15 test line\n',
);
});
test('-A option', t => {
const result = shell.grep('-A', 2, 'test*', 'test/resources/grep/file3');
t.falsy(shell.error());
t.is(
result.toString(),
'line2 test line\n' +
'line3 test line\n' +
'line4\n' +
'line5\n' +
'--\n' +
'line10 test line\n' +
'line11\n' +
'line12\n' +
'--\n' +
'line15 test line\n',
);
});
test('-A option, -B option', t => {
const result = shell.grep(
{ '-A': 2, '-B': 3 },
'test*',
'test/resources/grep/file3',
);
t.falsy(shell.error());
t.is(
result.toString(),
'line1\n' +
'line2 test line\n' +
'line3 test line\n' +
'line4\n' +
'line5\n' +
'--\n' +
'line7\n' +
'line8\n' +
'line9\n' +
'line10 test line\n' +
'line11\n' +
'line12\n' +
'line13\n' +
'line14\n' +
'line15 test line\n',
);
});
test('-A option, -B option, -n option', t => {
const result = shell.grep(
{ '-n': true, '-A': 2, '-B': 3 },
'test*',
'test/resources/grep/file3',
);
t.falsy(shell.error());
t.is(
result.toString(),
'1-line1\n' +
'2:line2 test line\n' +
'3:line3 test line\n' +
'4-line4\n' +
'5-line5\n' +
'--\n' +
'7-line7\n' +
'8-line8\n' +
'9-line9\n' +
'10:line10 test line\n' +
'11-line11\n' +
'12-line12\n' +
'13-line13\n' +
'14-line14\n' +
'15:line15 test line\n',
);
});
test('-C option', t => {
const result = shell.grep('-C', 3, 'test*', 'test/resources/grep/file3');
t.falsy(shell.error());
t.is(
result.toString(),
'line1\n' +
'line2 test line\n' +
'line3 test line\n' +
'line4\n' +
'line5\n' +
'line6\n' +
'line7\n' +
'line8\n' +
'line9\n' +
'line10 test line\n' +
'line11\n' +
'line12\n' +
'line13\n' +
'line14\n' +
'line15 test line\n',
);
});
test('-C option, small value', t => {
const result = shell.grep('-C', 1, 'test*', 'test/resources/grep/file3');
t.falsy(shell.error());
t.is(
result.toString(),
'line1\n' +
'line2 test line\n' +
'line3 test line\n' +
'line4\n' +
'--\n' +
'line9\n' +
'line10 test line\n' +
'line11\n' +
'--\n' +
'line14\n' +
'line15 test line\n',
);
});
test('-C option, large value', t => {
const result = shell.grep('-C', 100, 'test*', 'test/resources/grep/file3');
t.falsy(shell.error());
t.is(
result.toString(),
'line1\n' +
'line2 test line\n' +
'line3 test line\n' +
'line4\n' +
'line5\n' +
'line6\n' +
'line7\n' +
'line8\n' +
'line9\n' +
'line10 test line\n' +
'line11\n' +
'line12\n' +
'line13\n' +
'line14\n' +
'line15 test line\n',
);
});
test('-C option, add line separators', t => {
const result = shell.grep('-C', 0, 'test*', 'test/resources/grep/file3');
t.falsy(shell.error());
t.is(
result.toString(),
'line2 test line\n' +
'line3 test line\n' +
'--\n' +
'line10 test line\n' +
'--\n' +
'line15 test line\n',
);
});
test('-C option, -n option', t => {
const result = shell.grep('-nC', 3, 'test*', 'test/resources/grep/file3');
t.falsy(shell.error());
t.is(
result.toString(),
'1-line1\n' +
'2:line2 test line\n' +
'3:line3 test line\n' +
'4-line4\n' +
'5-line5\n' +
'6-line6\n' +
'7-line7\n' +
'8-line8\n' +
'9-line9\n' +
'10:line10 test line\n' +
'11-line11\n' +
'12-line12\n' +
'13-line13\n' +
'14-line14\n' +
'15:line15 test line\n',
);
});

15
test/resources/grep/file3 Normal file
View File

@ -0,0 +1,15 @@
line1
line2 test line
line3 test line
line4
line5
line6
line7
line8
line9
line10 test line
line11
line12
line13
line14
line15 test line