Syntactically-aware grep for JavaScript and TypeScript
Install it with npm install --global esgrep
or the equivalent using pnpm/yarn/etc, then use it as esgrep [OPTION...] PATTERN [FILE...]
. If FILE
is not precised, reads from stdin.
The CLI is basically a wrapper around find
and accepts the same options and a few more that handle help print and output format. This means that these are logically equivalent:
Reading from stdin:
echo 'const x: number = 10' | esgrep 'const x = ES_ANY'
find("const x: number = 10", "const x = ES_ANY");
Reading from files:
esgrep 'fetch(ES_ANY, { method: "POST" })' api.js lib.ts
find('fetch(ES_ANY, { method: "POST" })', readFileSomehow("api.js"));
find('fetch(ES_ANY, { method: "POST" })', readFileSomehow("lib.ts"));
Passing arguments:
esgrep --statement -- '(() => {})()' file.js
find("(() => {})()", readFileSomehow("file.js"), { statement: true });
Note
When you pass
esgrep --foo bar baz
,esgrep
has no way to know whetherbar
is the value of--foo
(current behavior) or if it is a positional argument just likebaz
and--foo
is a flag option.You can tell
esgrep
to explicitly start parsing positional arguments by using--
:esgrep --foo bar baz # Parsed options: { foo: 'bar' } # Parsed positional arguments: ['baz'] esgrep --foo -- bar baz # Parsed options: { foo: true } # parsed positional arguments: ['bar', 'baz']
Install it with npm install esgrep
or the equivalent using pnpm/yarn/etc, then import it:
import { find, findStrings } from "esgrep";
// or
const { find, findStrings } = require("esgrep");
For now, the lib only targets Node but it's in the roadmap to target Deno and the Web.
Types should be included in the build so refer to them for the exact types of arguments and returned values. This doc focuses on esgrep scenari rather than spec details.
pattern
and haystack
are string
s that represent javascript or typescript code which will be matched based on their AST (and not string representation).
options
is detailed in options.
find
is a Generator
that yields estree nodes.
import { find } from "esgrep";
const pattern = "const x = 10";
const haystack = "const x = /* a number */ 10; const y = 20;";
const matches = find(pattern, haystack);
for (const match of matches) {
console.log(match);
// {
// type: 'VariableDeclaration',
// declarations: [ ... ],
// kind: 'const',
// range: [ 0, 28 ],
// loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 28 } }
// }
}
If you're not comfortable with generators or don't want to use the perf and streaming capabilities they can provide, you can just spread it out into a plain array:
console.log([...matches]);
// [ { type: 'VariableDeclaration', ... }]
Basically the same as find
but iterates over matched strings rather that matched nodes.
import { findStrings } from "esgrep";
const pattern = "const x = 10";
const haystack = "const x = /* a number */ 10; const y = 20;";
console.log([...findStrings(pattern, haystack)]);
// [ 'const x = /* a number */ 10;' ]
Prints the synopsis and lists CLI options.
user@host$ esgrep -h
Defines the output format of the search
pretty
(default): easy to read output
user@host$ esgrep 'const tasks = ES_ANY' src/cli/main.ts
─────────────────────┐
src/cli/main.ts:20:2 │
┌─────────────────┘
20 │ const tasks =
21 │ paths.length === 0
22 │ ? [{ path: "stdin", read: readStdin }]
23 │ : paths.map((path) => ({ path, read: () => readFile(path) }));
oneline
: streams out lines of the shape $path:$line$column:$match_in_one_line
user@host$ esgrep --format oneline 'const tasks = ES_ANY' src/cli/main.ts
src/cli/main.ts:18:2:const tasks = paths.length === 0 ? [{ path: "stdin", read: readStdin }] : paths.map((path) => ({ path, read: () => readFile(path) }));
jsonl
: streams out lines of the shape { path: string, match: Node }
where Node
is an ESTree node
user@host$ esgrep user users.ts
{"path":"users.ts","match":{"type":"Identifier","name":"user","range":[148,152],"loc":{"start":{"line":4,"column":24},"end":{"line":4,"column":28}}}}
count
: do not show matches, only show the number of matches per file. column-separated
user@host$ esgrep user ./src/**/*.{ts,js}
./src/api.js:10
./src/users.js:4
./src/types.ts:0
Note
It is by design that ESGrep doesn't exactly match Grep output options, either because some option didn't make sense anymore, was not useful, or could be achieved by a little bit of bash-fu
grep --count ...
:esgrep --format count ...
grep --files-without-match ...
:esgrep --format count ... | grep ':0$' | cut -d ':' -f 1
grep --files-with-matches ...
:esgrep --format count ... | grep -v ':0$' | cut -d ':' -f 1
grep --with-filename ...
: always ongrep --no-filename ...
: always offgrep --line-number ...
: always ongrep --invert-match ...
: not usefulgrep --only-matching ...
: not useful
Include type annotations in the comparison
import { findStrings } from "esgrep";
const withTS = "const x: number = 10";
const withoutTS = "const x = 10";
console.log([...findStrings(withoutTS, withTS)]);
// [ 'const x: number = 10' ]
console.log([...findStrings(withoutTS, withTS, { ts: true })]);
// []
Differentiate between strings in single quotes, double quotes, and template literals
import { findStrings } from "esgrep";
const haystack = `
const single = 'hello';
const double = "hello";
const template = \`hello\`
`;
console.log([...findStrings('"hello"', haystack)]);
// [ "'hello'", '"hello"', '`hello`' ]
console.log([...findStrings('"hello"', haystack, { raw: true })]);
// [ "'hello'" ]
If the pattern is an expression statement, lookup the statement itself, and not the expression statement.
import { findStrings } from "esgrep";
const pattern = "10";
const haystack = "const x = 10";
console.log([...findStrings(pattern, haystack)]);
// [ '10' ]
console.log([...findStrings(pattern, haystack, { statement: true })]);
// []
Note The first search matches the
10
inconst x = 10
because it looks up all expressions of10
. In plain English, it looks up all the occurrences of "just" the number 10. That is the most intuitive and default behavior.The second search does not match the
10
inconst x = 10
because it looks up all statements of consisting of only the expression10
. In plain English, statements are anything that makes the exact same sense when adding a;
at the end.The difference is subtle and it usually takes people not familiar with the concept a few articles to have a good grasp on statements vs expressions. Why not start with this one?
Additionally to its options, patterns are allowed to be flexible thanks to ES expressions - special expressions that allow for something else than perfect matches.
Matches anything
import { findStrings } from "esgrep";
console.log([...findStrings("ES_ANY", "fn('foo', 'bar')")]);
// [
// "fn('foo', 'bar')", // matches the expression
// "fn('foo', 'bar')", // matches the statement (string is the same, not the node)
// "fn",
// "'foo'",
// "'bar'"
// ];
console.log([...findStrings("x = ES_ANY", "x = 10; x = 'hello'")]);
// [
// "x = 10",
// "x = 'hello'"
// ];
Matches anything but the first argument
import { findStrings } from "esgrep";
const pattern = "const x = ES_NOT(10)";
const haystack = "const x = 20";
console.log([...findStrings(pattern, haystack)]);
// [ 'const x = 20' ]
Matches if all expressions passed as argument match
import { findStrings } from "esgrep";
const pattern = `const x = ES_EVERY(
'hello',
"hello",
ES_ANY
)`;
const haystack = "const x = 'hello'";
console.log([...findStrings(pattern, haystack)]);
// [ "const x = 'hello'" ]
Matches if at least one expression passed as argument matches
import { findStrings } from "esgrep";
const pattern = `const x = ES_SOME(
"hello",
"goodbye"
)`;
const haystack = "const x = 'hello'";
console.log([...findStrings(pattern, haystack)]);
// [ "const x = 'hello'" ]
Want to report a bug? Don't understant the doc? Suggest an improvement? Open an issue!
Contributions are welcomed, the code is still small, clean, and all in TS, so it should be readable and extendable without additional guidance. For big changes, consider opening an issue before opening a PR.
Coded w/ ♡ by Nino Filiu