Repository: shelljs/shelljs
Pull request #1206: Add -B, -A, and -C options to grep
Author: login: abluescarab
Labels: name: feature
name: bash compat
description: Compatibility issues with bash or POSIX behavior
Created: 2025-04-04T02:31:27Z | Merged: 2025-04-19T15:26:51Z

Adds the `-B` (before context) and `-A` (after context) options to grep. I may have gone overboard on tests but I wanted to make sure that most feasible configurations work (combined, separate, with space, without space).

### Notes
* Modifies `globStart` to accept a function with a parameter `args`. This allows `-B` and `-A` to be specified separately and a function to determine where the post-regex arguments begin.
* Enforces these styles:
  * `.editorconfig`: Single quotes in JavaScript files
  * `.eslintrc.json`: `radix` option to `off` (don't require base for `parseInt()`)
  * `package.json`: Prettier `arrowParens` option to `avoid` (don't wrap single parameters in parentheses for arrow functions)

### Usage
```
grep -B <num> [args...]
grep -A <num> [args...]
```
### Examples
```bash
# show 3 lines before
grep -B3 string file1 file2 ...
grep -B 3 string file1 file2 ...

# show 2 lines after
grep -A2 string file1 file2 ...
grep -A 2 string file1 file2 ...

# show 3 lines before and 2 after
grep -B3A2 string file1 file2 ...
grep -B3 -A2 string file1 file2 ...

# show 3 lines before and after
grep -AB3 string file1 file2 ...
grep -AB 3 string file1 file2 ...
```


Diff:
diff --git a/README.md b/README.md
index e585e4be2..a0de67655 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/src/grep.js b/src/grep.js
index 63792e640..cfc83e40e 100644
--- a/src/grep.js
+++ b/src/grep.js
@@ -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;
diff --git a/test/grep.js b/test/grep.js
index e12ab80f9..4b64d6e76 100644
--- a/test/grep.js
+++ b/test/grep.js
@@ -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',
+  );
+});
diff --git a/test/resources/grep/file3 b/test/resources/grep/file3
new file mode 100644
index 000000000..e4bee9f8c
--- /dev/null
+++ b/test/resources/grep/file3
@@ -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


Reviews:
user: login: nfischer
body: Thanks for implementing this feature. I've provided some initial feedback that you can get started on now, but I'll need to spend more time reviewing this (expect more feedback later).
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-04T21:42:43Z
user: login: abluescarab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T00:24:35Z
user: login: abluescarab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T00:25:32Z
user: login: abluescarab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T02:00:05Z
user: login: abluescarab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T02:13:39Z
user: login: nfischer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-05T03:01:12Z
user: login: nfischer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-05T03:03:40Z
user: login: nfischer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-05T03:04:43Z
user: login: abluescarab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T03:51:10Z
user: login: abluescarab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T03:52:03Z
user: login: abluescarab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T03:52:27Z
user: login: abluescarab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T03:52:59Z
user: login: nfischer
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-06T05:13:06Z
user: login: abluescarab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T04:34:22Z
user: login: abluescarab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T04:34:32Z
user: login: abluescarab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T04:35:18Z
user: login: abluescarab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T04:35:24Z
user: login: abluescarab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T04:35:31Z
user: login: nfischer
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-10T07:27:00Z
user: login: nfischer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T08:30:13Z
user: login: abluescarab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T06:58:44Z
user: login: abluescarab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T07:02:25Z
user: login: nfischer
body: LGTM, but with a few more suggestions.

Thanks for working on this feature!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-13T08:37:34Z
user: login: abluescarab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T01:53:40Z
user: login: abluescarab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T01:54:07Z
user: login: nfischer
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-19T15:24:59Z

Review comments:
diff_hunk: @@ -10,5 +10,8 @@ indent_style = space
 insert_final_newline = true
 trim_trailing_whitespace = true
 
+[*.js]
+quote_type = single
path: .editorconfig
user: login: nfischer
body: Please split this into a separate PR
created_at: 2025-04-04T21:35:44Z
author_association: MEMBER
diff_hunk: @@ -91,5 +91,8 @@
   },
   "engines": {
     "node": ">=18"
+  },
+  "prettier": {
path: package.json
user: login: nfischer
body: What is this?
created_at: 2025-04-04T21:36:21Z
author_association: MEMBER
diff_hunk: @@ -447,7 +447,11 @@ function wrap(cmd, fn, options) {
         // Perform glob-expansion on all arguments after globStart, but preserve
         // the arguments before it (like regexes for sed and grep)
         if (!config.noglob && options.allowGlobbing === true) {
-          args = args.slice(0, options.globStart).concat(expand(args.slice(options.globStart)));
+          var globStart =
+            options.globStart instanceof Function
path: src/common.js
user: login: nfischer
body: Is this strictly necessary? We were able to implement `head -n <num>` without this. See https://github.com/shelljs/shelljs#head-n-num-file--file-
created_at: 2025-04-04T21:38:12Z
author_association: MEMBER
diff_hunk: @@ -174,3 +174,118 @@ test('the pattern looks like an option', t => {
   t.falsy(shell.error());
   t.is(result.toString(), '-v\n-vv\n');
 });
+
+//
+// Before & after contexts
+//
+const contextOptions = ['test*', 'test/resources/grep/file3'];
+
+// key is expected results and value is an array of arguments to test
+const beforeAndAfterTests = {
+  // before context
+  'line1\nline2 test line\n--\nline1\nline2 test line\nline3 test line\n--\nline7\nline8\nline9\nline10 test line\n--\nline12\nline13\nline14\nline15 test line\n':
+    [['-B3'], ['-B', 3], ['-B3i'], ['-Bi', 3], ['-iB3'], ['-iB', 3]],
+  // before context + line numbers
+  '1-line1\n2:line2 test line\n--\n1-line1\n2-line2 test line\n3:line3 test line\n--\n7-line7\n8-line8\n9-line9\n10:line10 test line\n--\n12-line12\n13-line13\n14-line14\n15:line15 test line\n':
path: test/grep.js
user: login: nfischer
body: I think it's too complicated to keep this all as one mega test case which loops over the array. Can you split this into multiple different test cases?

Simpler is going to be better for me to review.
created_at: 2025-04-04T21:41:19Z
author_association: MEMBER
diff_hunk: @@ -91,5 +91,8 @@
   },
   "engines": {
     "node": ">=18"
+  },
+  "prettier": {
path: package.json
user: login: abluescarab
body: Prettier can enforce its rules using a `prettier` key in `package.json` or with a separate file. The default option for Prettier is to always wrap single parameters in arrow functions with parentheses. This meant that when formatting code with Prettier,
```js
test('test name', t => { ... });
```
would become
```js
test('test name', (t) => { ... });
```
This option disables that formatting.
created_at: 2025-04-05T00:24:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -10,5 +10,8 @@ indent_style = space
 insert_final_newline = true
 trim_trailing_whitespace = true
 
+[*.js]
+quote_type = single
path: .editorconfig
user: login: abluescarab
body: Moved to #1207.
created_at: 2025-04-05T00:25:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -174,3 +174,118 @@ test('the pattern looks like an option', t => {
   t.falsy(shell.error());
   t.is(result.toString(), '-v\n-vv\n');
 });
+
+//
+// Before & after contexts
+//
+const contextOptions = ['test*', 'test/resources/grep/file3'];
+
+// key is expected results and value is an array of arguments to test
+const beforeAndAfterTests = {
+  // before context
+  'line1\nline2 test line\n--\nline1\nline2 test line\nline3 test line\n--\nline7\nline8\nline9\nline10 test line\n--\nline12\nline13\nline14\nline15 test line\n':
+    [['-B3'], ['-B', 3], ['-B3i'], ['-Bi', 3], ['-iB3'], ['-iB', 3]],
+  // before context + line numbers
+  '1-line1\n2:line2 test line\n--\n1-line1\n2-line2 test line\n3:line3 test line\n--\n7-line7\n8-line8\n9-line9\n10:line10 test line\n--\n12-line12\n13-line13\n14-line14\n15:line15 test line\n':
path: test/grep.js
user: login: abluescarab
body: \<snip original\>
Once I get `globStart` back to normal, the number of tests will reduce and I can split them out.
created_at: 2025-04-05T02:00:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -447,7 +447,11 @@ function wrap(cmd, fn, options) {
         // Perform glob-expansion on all arguments after globStart, but preserve
         // the arguments before it (like regexes for sed and grep)
         if (!config.noglob && options.allowGlobbing === true) {
-          args = args.slice(0, options.globStart).concat(expand(args.slice(options.globStart)));
+          var globStart =
+            options.globStart instanceof Function
path: src/common.js
user: login: abluescarab
body: I'll check it out!
created_at: 2025-04-05T02:13:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -91,5 +91,8 @@
   },
   "engines": {
     "node": ">=18"
+  },
+  "prettier": {
path: package.json
user: login: nfischer
body: ShellJS isn't configured to use `prettier` and we don't have that package as a dependency. Realistically, I can't accept configs for stuff that I don't use for this project.

I won't object if you want to use `prettier` for your own changes, as long as the final PR also passes `npm run lint`. But you would have to find a way to do this without adding extra configs in our project.
created_at: 2025-04-05T03:01:12Z
author_association: MEMBER
diff_hunk: @@ -2,16 +2,55 @@ var common = require('./common');
 var fs = require('fs');
 
 common.register('grep', _grep, {
-  globStart: 2, // don't glob-expand the regex
+  globStart: _getGlobStart, // don't glob-expand the regex
   canReceivePipe: true,
-  cmdOptions: {
-    'v': 'inverse',
-    'l': 'nameOnly',
-    'i': 'ignoreCase',
-    'n': 'lineNumber',
-  },
 });
 
+// Gets the index of the first argument after `regex_filter`
+function _getGlobStart(args) {
+  return args[0] === '--'
+    ? 2
+    : Array.prototype.findIndex.call(args, function (v) {
+        return (
+          v instanceof RegExp ||
+          (typeof v === 'string' && v !== '' && !v.startsWith('-'))
+        );
+      }) + 1;
+}
+
+// Gets the value of the `beforeContext` or `afterContext` options, removing
+// them from the `options` string
+function _getContextOption(options, option) {
+  var regex = new RegExp('-(\\w*)' + option + '([A-Za-z]*) ?(\\d*)(.*)');
+  var value = null;
+  var replaced = options.replace(regex, function (_, before, after, val, rest) {
+    var result = '';
+    if (before || after || rest) {
+      result = (before + after + rest).trim();
+      if (result && !result.startsWith('-')) {
+        result = '-' + result;
+      }
+    }
+    value = parseInt(val);
path: src/grep.js
user: login: nfischer
body: Please specify `parseInt(val, 10)`. Then you can remove the `radix` eslint suppression.
created_at: 2025-04-05T03:03:39Z
author_association: MEMBER
diff_hunk: @@ -2,16 +2,55 @@ var common = require('./common');
 var fs = require('fs');
 
 common.register('grep', _grep, {
-  globStart: 2, // don't glob-expand the regex
+  globStart: _getGlobStart, // don't glob-expand the regex
   canReceivePipe: true,
-  cmdOptions: {
-    'v': 'inverse',
-    'l': 'nameOnly',
-    'i': 'ignoreCase',
-    'n': 'lineNumber',
-  },
 });
 
+// Gets the index of the first argument after `regex_filter`
+function _getGlobStart(args) {
+  return args[0] === '--'
+    ? 2
+    : Array.prototype.findIndex.call(args, function (v) {
+        return (
+          v instanceof RegExp ||
+          (typeof v === 'string' && v !== '' && !v.startsWith('-'))
+        );
+      }) + 1;
+}
+
+// Gets the value of the `beforeContext` or `afterContext` options, removing
+// them from the `options` string
+function _getContextOption(options, option) {
+  var regex = new RegExp('-(\\w*)' + option + '([A-Za-z]*) ?(\\d*)(.*)');
+  var value = null;
+  var replaced = options.replace(regex, function (_, before, after, val, rest) {
+    var result = '';
+    if (before || after || rest) {
+      result = (before + after + rest).trim();
+      if (result && !result.startsWith('-')) {
+        result = '-' + result;
+      }
+    }
+    value = parseInt(val);
path: src/grep.js
user: login: nfischer
body: We have some code in shelljs which actually does parseInt using a different radix (see chmod.js and exec.js), so I would like all calls to be explicit about the radix.
created_at: 2025-04-05T03:04:43Z
author_association: MEMBER
diff_hunk: @@ -91,5 +91,8 @@
   },
   "engines": {
     "node": ">=18"
+  },
+  "prettier": {
path: package.json
user: login: abluescarab
body: That's understandable, I've removed it.
created_at: 2025-04-05T03:51:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -447,7 +447,11 @@ function wrap(cmd, fn, options) {
         // Perform glob-expansion on all arguments after globStart, but preserve
         // the arguments before it (like regexes for sed and grep)
         if (!config.noglob && options.allowGlobbing === true) {
-          args = args.slice(0, options.globStart).concat(expand(args.slice(options.globStart)));
+          var globStart =
+            options.globStart instanceof Function
path: src/common.js
user: login: abluescarab
body: The latest changes have been pushed which revert `globStart` back to normal. Thanks for linking to `head` so I could figure it out from that!
created_at: 2025-04-05T03:52:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -174,3 +174,118 @@ test('the pattern looks like an option', t => {
   t.falsy(shell.error());
   t.is(result.toString(), '-v\n-vv\n');
 });
+
+//
+// Before & after contexts
+//
+const contextOptions = ['test*', 'test/resources/grep/file3'];
+
+// key is expected results and value is an array of arguments to test
+const beforeAndAfterTests = {
+  // before context
+  'line1\nline2 test line\n--\nline1\nline2 test line\nline3 test line\n--\nline7\nline8\nline9\nline10 test line\n--\nline12\nline13\nline14\nline15 test line\n':
+    [['-B3'], ['-B', 3], ['-B3i'], ['-Bi', 3], ['-iB3'], ['-iB', 3]],
+  // before context + line numbers
+  '1-line1\n2:line2 test line\n--\n1-line1\n2-line2 test line\n3:line3 test line\n--\n7-line7\n8-line8\n9-line9\n10:line10 test line\n--\n12-line12\n13-line13\n14-line14\n15:line15 test line\n':
path: test/grep.js
user: login: abluescarab
body: The tests have been split out separately and simplified.
created_at: 2025-04-05T03:52:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,16 +2,55 @@ var common = require('./common');
 var fs = require('fs');
 
 common.register('grep', _grep, {
-  globStart: 2, // don't glob-expand the regex
+  globStart: _getGlobStart, // don't glob-expand the regex
   canReceivePipe: true,
-  cmdOptions: {
-    'v': 'inverse',
-    'l': 'nameOnly',
-    'i': 'ignoreCase',
-    'n': 'lineNumber',
-  },
 });
 
+// Gets the index of the first argument after `regex_filter`
+function _getGlobStart(args) {
+  return args[0] === '--'
+    ? 2
+    : Array.prototype.findIndex.call(args, function (v) {
+        return (
+          v instanceof RegExp ||
+          (typeof v === 'string' && v !== '' && !v.startsWith('-'))
+        );
+      }) + 1;
+}
+
+// Gets the value of the `beforeContext` or `afterContext` options, removing
+// them from the `options` string
+function _getContextOption(options, option) {
+  var regex = new RegExp('-(\\w*)' + option + '([A-Za-z]*) ?(\\d*)(.*)');
+  var value = null;
+  var replaced = options.replace(regex, function (_, before, after, val, rest) {
+    var result = '';
+    if (before || after || rest) {
+      result = (before + after + rest).trim();
+      if (result && !result.startsWith('-')) {
+        result = '-' + result;
+      }
+    }
+    value = parseInt(val);
path: src/grep.js
user: login: abluescarab
body: This line is no longer needed.
created_at: 2025-04-05T03:52:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -39,7 +44,17 @@ function _grep(options, regex, files) {
 
   if (!files && !pipe) common.error('no paths given', 2);
 
-  files = [].slice.call(arguments, 2);
+  var idx = 2;
+  if (options.beforeContext === true) {
path: src/grep.js
user: login: nfischer
body: I think this code may be clearer with:

```js
if (options.beforeContext && typeof options.beforeContext === 'boolean') {
}
```

Alternatively, you can leave a comment explaining why the code is doing a strict equality comparison rather than checking for truthiness.
created_at: 2025-04-06T04:52:36Z
author_association: MEMBER
diff_hunk: @@ -22,12 +24,15 @@ 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.
path: src/grep.js
user: login: nfischer
body: Do you want to implement `-C` too? Seems like this is the same as `-AB <num>` which you've already implemented.
created_at: 2025-04-06T05:01:45Z
author_association: MEMBER
diff_hunk: @@ -174,3 +174,225 @@ 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' +
+      '--\n' +
+      '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' +
+      '--\n' +
+      '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, separated', 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' +
+      '--\n' +
+      'line3 test line\n' +
+      'line4\n' +
+      'line5\n' +
+      '--\n' +
+      'line10 test line\n' +
+      'line11\n' +
+      'line12\n' +
+      '--\n' +
+      'line15 test line\n\n'
+  );
+});
+
+test('-A option, -n option', t => {
+  const result = shell.grep('-nA', 2, 'test*', 'test/resources/grep/file3');
+  t.falsy(shell.error());
+  t.is(
+    result.toString(),
+    '2:line2 test line\n' +
+      '3-line3 test line\n' +
+      '4-line4\n' +
+      '--\n' +
+      '3:line3 test line\n' +
+      '4-line4\n' +
+      '5-line5\n' +
+      '--\n' +
+      '10:line10 test line\n' +
+      '11-line11\n' +
+      '12-line12\n' +
+      '--\n' +
+      '15:line15 test line\n\n'
+  );
+});
+
+test('-AB option, separated, same', t => {
+  const result = shell.grep('-AB', 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' +
+      'line1\n' +
+      'line2 test line\n' +
+      'line3 test line\n' +
+      'line4\n' +
+      'line5\n' +
+      'line6\n' +
+      '--\n' +
+      'line7\n' +
+      'line8\n' +
+      'line9\n' +
+      'line10 test line\n' +
+      'line11\n' +
+      'line12\n' +
+      'line13\n' +
+      '--\n' +
+      'line12\n' +
+      'line13\n' +
+      'line14\n' +
+      'line15 test line\n\n'
+  );
+});
+
+test('-AB option, separated, different', 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' +
+      '--\n' +
+      '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' +
+      '--\n' +
+      'line12\n' +
+      'line13\n' +
+      'line14\n' +
+      'line15 test line\n\n'
+  );
+});
+
+test('-AB option, -n option, separated, same', t => {
+  const result = shell.grep('-nAB', 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' +
+      '1-line1\n' +
+      '2-line2 test line\n' +
+      '3:line3 test line\n' +
+      '4-line4\n' +
+      '5-line5\n' +
+      '6-line6\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' +
+      '--\n' +
+      '12-line12\n' +
+      '13-line13\n' +
+      '14-line14\n' +
+      '15:line15 test line\n\n'
+  );
+});
+
+test('-AB option, -n option, separated, different', 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' +
+      '--\n' +
+      '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' +
+      '--\n' +
+      '12-line12\n' +
path: test/grep.js
user: login: nfischer
body: This extra line separator doesn't match the behavior I see in unix grep. If the context regions overlap, then unix grep's behavior is to omit the line separator and treat it like one big region.

```
$ grep -A 2 -B 3 'test' test.txt
line1
line2 test line
line3 test line
line4
line5
--
line7
line8
line9
line10 test line
line11
line12
line13
line14
line15 test line
```
created_at: 2025-04-06T05:04:29Z
author_association: MEMBER
diff_hunk: @@ -174,3 +174,225 @@ 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' +
+      '--\n' +
+      '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' +
+      '--\n' +
+      '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, separated', 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' +
+      '--\n' +
+      'line3 test line\n' +
+      'line4\n' +
+      'line5\n' +
+      '--\n' +
+      'line10 test line\n' +
+      'line11\n' +
+      'line12\n' +
+      '--\n' +
+      'line15 test line\n\n'
+  );
+});
+
+test('-A option, -n option', t => {
path: test/grep.js
user: login: nfischer
body: I think you can omit this case. I think `test('-B option, -n option')` and `test('-A option, separated')` cover this case nicely.
created_at: 2025-04-06T05:06:02Z
author_association: MEMBER
diff_hunk: @@ -22,12 +24,15 @@ 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.
 //@
 //@ Examples:
 //@
 //@ ```javascript
 //@ grep('-v', 'GLOBAL_VARIABLE', '*.js');
 //@ grep('GLOBAL_VARIABLE', '*.js');
+//@ grep('-B', 3, 'GLOBAL_VARIABLE', '*.js');
path: src/grep.js
user: login: nfischer
body: Can you please also document a case showing the object-style of passing the `-B` option?
created_at: 2025-04-06T05:12:39Z
author_association: MEMBER
diff_hunk: @@ -39,7 +44,17 @@ function _grep(options, regex, files) {
 
   if (!files && !pipe) common.error('no paths given', 2);
 
-  files = [].slice.call(arguments, 2);
+  var idx = 2;
+  if (options.beforeContext === true) {
path: src/grep.js
user: login: abluescarab
body: I've added a comment explaining that line instead of rewriting it (maintains consistency with the rest of the code).
created_at: 2025-04-07T04:34:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,12 +24,15 @@ 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.
path: src/grep.js
user: login: abluescarab
body: Done!
created_at: 2025-04-07T04:34:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -174,3 +174,225 @@ 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' +
+      '--\n' +
+      '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' +
+      '--\n' +
+      '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, separated', 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' +
+      '--\n' +
+      'line3 test line\n' +
+      'line4\n' +
+      'line5\n' +
+      '--\n' +
+      'line10 test line\n' +
+      'line11\n' +
+      'line12\n' +
+      '--\n' +
+      'line15 test line\n\n'
+  );
+});
+
+test('-A option, -n option', t => {
+  const result = shell.grep('-nA', 2, 'test*', 'test/resources/grep/file3');
+  t.falsy(shell.error());
+  t.is(
+    result.toString(),
+    '2:line2 test line\n' +
+      '3-line3 test line\n' +
+      '4-line4\n' +
+      '--\n' +
+      '3:line3 test line\n' +
+      '4-line4\n' +
+      '5-line5\n' +
+      '--\n' +
+      '10:line10 test line\n' +
+      '11-line11\n' +
+      '12-line12\n' +
+      '--\n' +
+      '15:line15 test line\n\n'
+  );
+});
+
+test('-AB option, separated, same', t => {
+  const result = shell.grep('-AB', 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' +
+      'line1\n' +
+      'line2 test line\n' +
+      'line3 test line\n' +
+      'line4\n' +
+      'line5\n' +
+      'line6\n' +
+      '--\n' +
+      'line7\n' +
+      'line8\n' +
+      'line9\n' +
+      'line10 test line\n' +
+      'line11\n' +
+      'line12\n' +
+      'line13\n' +
+      '--\n' +
+      'line12\n' +
+      'line13\n' +
+      'line14\n' +
+      'line15 test line\n\n'
+  );
+});
+
+test('-AB option, separated, different', 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' +
+      '--\n' +
+      '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' +
+      '--\n' +
+      'line12\n' +
+      'line13\n' +
+      'line14\n' +
+      'line15 test line\n\n'
+  );
+});
+
+test('-AB option, -n option, separated, same', t => {
+  const result = shell.grep('-nAB', 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' +
+      '1-line1\n' +
+      '2-line2 test line\n' +
+      '3:line3 test line\n' +
+      '4-line4\n' +
+      '5-line5\n' +
+      '6-line6\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' +
+      '--\n' +
+      '12-line12\n' +
+      '13-line13\n' +
+      '14-line14\n' +
+      '15:line15 test line\n\n'
+  );
+});
+
+test('-AB option, -n option, separated, different', 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' +
+      '--\n' +
+      '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' +
+      '--\n' +
+      '12-line12\n' +
path: test/grep.js
user: login: abluescarab
body: I've fixed this, but I had to write an extra loop to solve the issue. That means it will be minimally slower but I'm not sure if there's a better option.
created_at: 2025-04-07T04:35:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -174,3 +174,225 @@ 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' +
+      '--\n' +
+      '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' +
+      '--\n' +
+      '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, separated', 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' +
+      '--\n' +
+      'line3 test line\n' +
+      'line4\n' +
+      'line5\n' +
+      '--\n' +
+      'line10 test line\n' +
+      'line11\n' +
+      'line12\n' +
+      '--\n' +
+      'line15 test line\n\n'
+  );
+});
+
+test('-A option, -n option', t => {
path: test/grep.js
user: login: abluescarab
body: Done!
created_at: 2025-04-07T04:35:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,12 +24,15 @@ 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.
 //@
 //@ Examples:
 //@
 //@ ```javascript
 //@ grep('-v', 'GLOBAL_VARIABLE', '*.js');
 //@ grep('GLOBAL_VARIABLE', '*.js');
+//@ grep('-B', 3, 'GLOBAL_VARIABLE', '*.js');
path: src/grep.js
user: login: abluescarab
body: Done!
created_at: 2025-04-07T04:35:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,23 +86,101 @@ function _grep(options, regex, files) {
       }
     } else {
       var lines = contents.split('\n');
+      var matches = [];
+
+      //  If before or after not given but context is, update values
+      if (options.context) {
+        if (options.beforeContext === false) {
+          options.beforeContext = options.context;
+        }
+        if (options.afterContext === false) {
+          options.afterContext = options.context;
+        }
+      }
+
       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: 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) {
path: src/grep.js
user: login: nfischer
body: It looks like this will involve a doubly-nested loop (O(n^2) complexity). Would it be possible to instead write this with a single loop (O(n) complexity)?

I think we can iterate through and maintain a queue of a max size B. Once we see a regex match, output the queue of size B (this is all of the "before" context) and output the current line. You can do a similar approach for the "after" context. If you see another regex match before the "after" context is complete, then just print the regex match and start over the with "after" queue.

Let me know if you feel up to trying this out, otherwise I can try to work on this after the PR lands.
created_at: 2025-04-10T07:17:16Z
author_association: MEMBER
diff_hunk: @@ -62,23 +86,101 @@ function _grep(options, regex, files) {
       }
     } else {
       var lines = contents.split('\n');
+      var matches = [];
+
+      //  If before or after not given but context is, update values
+      if (options.context) {
+        if (options.beforeContext === false) {
+          options.beforeContext = options.context;
+        }
+        if (options.afterContext === false) {
+          options.afterContext = options.context;
+        }
+      }
+
       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: 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)) {
path: src/grep.js
user: login: nfischer
body: This condition seems redundant to me. How could the result already be in the array if the object was created fresh at the start of the loop iteration (line 105)?

One way to double check this would be to temporarily change this to:

```
if (!matches.includes(result)) {
  matches.push(result);
} else {
  console.log('Do not submit this part. This line exists only to check if it shows up in the test coverage result');
}
```
created_at: 2025-04-10T07:20:35Z
author_association: MEMBER
diff_hunk: @@ -62,23 +86,101 @@ function _grep(options, regex, files) {
       }
     } else {
       var lines = contents.split('\n');
+      var matches = [];
+
+      //  If before or after not given but context is, update values
+      if (options.context) {
+        if (options.beforeContext === false) {
+          options.beforeContext = options.context;
+        }
+        if (options.afterContext === false) {
+          options.afterContext = options.context;
+        }
+      }
+
       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: 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)) {
path: src/grep.js
user: login: nfischer
body: I forgot to mention, but you can run code coverage with `npm run test-with-coverage`. Be warned, this will run a bit slower than a regular `npm test`. You'll need to read through the coverage report carefully to see if this line number is in the "uncovered lines" column.
created_at: 2025-04-10T08:30:13Z
author_association: MEMBER
diff_hunk: @@ -62,23 +86,101 @@ function _grep(options, regex, files) {
       }
     } else {
       var lines = contents.split('\n');
+      var matches = [];
+
+      //  If before or after not given but context is, update values
+      if (options.context) {
+        if (options.beforeContext === false) {
+          options.beforeContext = options.context;
+        }
+        if (options.afterContext === false) {
+          options.afterContext = options.context;
+        }
+      }
+
       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: 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)) {
path: src/grep.js
user: login: abluescarab
body: On line 117, `result` is assigned to the last match, so it's no longer a new object. I've been using `test-with-coverage` while writing tests, and it confirms that this line is indeed being covered!
created_at: 2025-04-11T06:58:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,23 +86,101 @@ function _grep(options, regex, files) {
       }
     } else {
       var lines = contents.split('\n');
+      var matches = [];
+
+      //  If before or after not given but context is, update values
+      if (options.context) {
+        if (options.beforeContext === false) {
+          options.beforeContext = options.context;
+        }
+        if (options.afterContext === false) {
+          options.afterContext = options.context;
+        }
+      }
+
       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: 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) {
path: src/grep.js
user: login: abluescarab
body: I’ve been trying to get it down to O(n) for a while today and still haven’t figured it out. Would you mind giving it a go?
created_at: 2025-04-11T07:02:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -174,3 +174,166 @@ 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');
path: test/grep.js
user: login: nfischer
body: Can you add a test case for `{'-C': 2}` or `{'-C': 1}`? I noticed these test cases use C values which are so large that the output doesn't include any line separators. That's actually a good thing to test, but we should additionally have a test for smaller C values just to verify the code does add the line separators when needed.
created_at: 2025-04-13T08:32:22Z
author_association: MEMBER
diff_hunk: @@ -174,3 +174,166 @@ test('the pattern looks like an option', t => {
   t.falsy(shell.error());
   t.is(result.toString(), '-v\n-vv\n');
 });
+
+//
+// Before & after contexts
path: test/grep.js
user: login: nfischer
body: Can you add a few more test cases?

* `-C 0` should behave like regular grep, except it should insert line separators
* `-C -1` should trigger an error with `common.error('grep: -1: invalid context length argument')`
* `-C 100` (some number bigger than the total input file length) should just print the whole file without errors, assuming there is at least one match for the regex pattern
created_at: 2025-04-13T08:36:55Z
author_association: MEMBER
diff_hunk: @@ -174,3 +174,166 @@ test('the pattern looks like an option', t => {
   t.falsy(shell.error());
   t.is(result.toString(), '-v\n-vv\n');
 });
+
+//
+// Before & after contexts
path: test/grep.js
user: login: abluescarab
body: Done!
created_at: 2025-04-16T01:53:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -174,3 +174,166 @@ 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');
path: test/grep.js
user: login: abluescarab
body: I added a test case for `{'-C', 1}`.
created_at: 2025-04-16T01:54:07Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-05T03:42:32Z
body: ## [Codecov](https://app.codecov.io/gh/shelljs/shelljs/pull/1206?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=shelljs) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 97.46%. Comparing base [(`75d26de`)](https://app.codecov.io/gh/shelljs/shelljs/commit/75d26de5b31f71bc8784e3d9329375e83c7b6d49?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=shelljs) to head [(`1fc99f6`)](https://app.codecov.io/gh/shelljs/shelljs/commit/1fc99f6ecaff2e5b3c2a5de06a2b02ed753cc355?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=shelljs).
> Report is 2 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1206      +/-   ##
==========================================
+ Coverage   97.37%   97.46%   +0.08%     
==========================================
  Files          35       35              
  Lines        1410     1459      +49     
==========================================
+ Hits         1373     1422      +49     
  Misses         37       37              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/shelljs/shelljs/pull/1206?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=shelljs).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=shelljs).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: abluescarab
created_at: 2025-04-05T03:56:54Z
body: Sorry about the commit mess. I'm not sure why checks are failing on macos and ubuntu now.
author_association: CONTRIBUTOR
user: login: nfischer
created_at: 2025-04-06T04:51:23Z
body: > Sorry about the commit mess. I'm not sure why checks are failing on macos and ubuntu now.

Oh, this should be a simple fix. Can you run `npm run gendocs` in a terminal? This will regenerate README.md, then you can commit the README changes. That's probably the issue.

If that doesn't fix it, then try running the tests locally and run `git status`. It looks like there's some uncommitted file after the tests finish, and `git status` should tell you exactly what file that is.
author_association: MEMBER
user: login: abluescarab
created_at: 2025-04-06T05:02:35Z
body: > Oh, this should be a simple fix. Can you run `npm run gendocs` in a terminal? This will regenerate README.md, then you can commit the README changes. That's probably the issue.

Hey, that worked, thanks! Hopefully everything looks good now.
author_association: CONTRIBUTOR
user: login: nfischer
created_at: 2025-05-09T19:20:18Z
body: Released in shelljs v0.10. Thanks for the contribution!
author_association: MEMBER