Find and replace with both halves stated: what the pattern matches, and whether $1 and $& in the replacement are patterns or text
Initializing in your browser…
View, sort and edit a CSV, with an undo stack that goes back exactly one step and a parse report that tells a broken quote from a ragged row
Filter rows with each condition stating its rule: a cell that is not a number does not match a comparison, and an empty cell is not a zero
Rename columns under a stated convention, with two that would end up with the same name numbered rather than left to collide
A price column holds "$1,299.99" and a note column holds the text "TBD". You want the prices as plain numbers, and the notes to read "$&!" exactly as typed.
Two passes over four rows
id,price,note 1,"$1,299.99",TBD 2,$50.00,TBD 3,$7.05,paid 1. In "price", regex [$,] replaced with nothing 2. In "note", the text TBD replaced with $&!
Result
id,price,note 1,1299.99,$&! 2,50.00,$&! 3,7.05,paid pass 1: 4 matches in 3 of 3 cells searched, 3 cells changed pass 2: 2 matches, read exactly as typed switch to "With $1 and $& patterns" and pass 2 writes TBD! instead
Two things here were impossible before. Replacing with nothing is a deletion, and the previous version treated an empty replacement box as "no replacement asked for" and quietly did nothing while still offering to replace the matches it had found. And the replacement is inserted exactly as typed unless you ask otherwise, so $&! stays $&! rather than becoming the matched text; the reading is a control on the page, because $1 and $& are the point of a regular expression and a nuisance everywhere else. The counts separate matches from cells, so 4 matches in 3 cells is legible: one row had both a dollar sign and a comma.
Search a CSV for text or a regular expression and replace what you find, with two questions answered out loud rather than guessed at: what the pattern matches, and what the replacement means. Scored against Python's own regex engine and the ECMA-262 substitution table on a 45 case corpus, the previous version got 29 of them right and hung the page outright on five.
Measured on 2026-09-02 against a corpus whose right answer per case comes from Python's `re` for what a pattern matches and from ECMA-262 22.1.3.19 GetSubstitution, written out, for what a replacement means. Baseline 29 of 45 correct, with 5 cases in which the page never came back. After: 45 of 45, in the module and through the browser.
The worst fault was not a wrong answer, it was no answer. Counting matches used a hand-written `while ((m = re.exec(cell)) !== null)` loop with a global pattern. Under ECMA-262 22.2.7.2 a global match that consumes nothing does not move lastIndex, so the caller has to advance it, and this loop did not. Typing `a*`, `x?`, `^` or `\b` into the search box therefore looped forever inside a memo that runs during render, and the tab stopped responding. Measured: after typing `a*` the page did not answer `1 + 1`. The scan now advances by one code point after a zero-length match, per 22.2.9.2, and `a*` over the whole corpus finds 1,087 matches in about 120 milliseconds.
The replacement was always a substitution template. Replacing something with the literal text `$&` inserted the matched text instead, `$5.00` was at the mercy of whether a fifth capture group happened to exist, and none of this was true of the search box, which escaped metacharacters so a dot meant a dot. The two readings are now a control. "Exactly as typed" inserts the characters you typed, which is the default and is what a plain-text replace should always have done. "With $1 and $& patterns" applies the ECMA-262 table in full: $$, $&, $backtick, $apostrophe, $n, $nn and $<name>, with a group that did not participate substituting as nothing. Turning on Use regex turns the pattern reading on with it, because capture groups are the reason to be there.
Deleting text was impossible. The preview guarded on `if (!searchTerm || !replaceTerm) return csvData`, and an empty string is falsy, so leaving the replacement box empty returned the file unchanged while the tool still offered to replace the matches it had found and reported them as replaced. An empty replacement is a deletion now and is carried out, and the summary says so.
A pattern that cannot be compiled was caught and turned into zero matches with no message, which looks exactly like a pattern that matched nothing. A mistyped `[abc` now reports the engine's own message and says that nothing was searched, which is not the same as nothing matching.
Whole word did nothing at all in regex mode: the word boundaries were added only on the plain-text branch. It applies in both now, and it is defined as "not adjacent to a letter, digit, mark or underscore" rather than as the \b assertion, because \b is a boundary BETWEEN a word and a non-word character and therefore depends on context: `\bC\+\+\b` matches inside `C++11`. Measured on the corpus, the old rule matched C++ inside C++11 and refused café before a comma; the new one does neither.
Patterns compile in Unicode mode wherever they can, which is what makes a dot one code point rather than one UTF-16 code unit, makes the Kelvin sign U+212A fold with a lower case k, makes \p{L} available, and makes a zero-length scan step over an emoji rather than through it (`x?` over `a😀b` used to produce two lone surrogates). The few patterns that are legal without the u flag and errors with it fall back, and the page says which mode is in force.
A one-column CSV was refused outright, because papaparse reports UndetectableDelimiter for a file with no delimiter in it to detect and every Delimiter-type error was treated as fatal. Its own documentation classes that as a warning; the parse has already defaulted to a comma and is correct. And the download goes through the RFC 4180 writer now: CRLF records per section 2.1, quoting as a minimum rather than a preference, and a byte order mark so a spreadsheet reads the file as UTF-8. Python's csv module reads a cell holding a comma, a cell holding a quote and a cell holding a newline back out of the download unchanged.
Delete the currency symbols and grouped-thousands commas with one regex and an empty replacement, so the column can be read as numbers.
Scope the replace to one column and watch the count: 5 matches in 5 of 20 cells searched tells you the rule hit what you meant before you commit.
Reorder a name, restructure a date, or rebuild an identifier with $1 and $2, with the substitution reading turned on explicitly so nothing else in the file is at the mercy of a dollar sign.
Turn on "Search the header row" and the same rule applies to the header, which the previous version never touched and never said it was skipping.
Turn on Use regex, search [$,] and leave the replacement empty, scoped to the price column. Four matches across three cells become 1299.99, 50.00 and 7.05.
Search TBD and replace with $5.00 while the reading is "Exactly as typed". The replacement lands as $5.00. Under the pattern reading it also lands as $5.00, because $5 is a group that does not exist, but $&! would become the matched text.
Turn on Use regex, which switches the reading to patterns, search ^(\w+), (\w+)$ and replace with $2 $1 to turn "Smith, John" into "John Smith".
Because a global regular expression that matches the empty string does not advance its own scan position: ECMA-262 leaves that to the caller, and the loop counting matches never did it. The scan now steps forward by one code point after a zero-length match, so a*, x?, ^ and \b all terminate. That was measured, not assumed: after typing a* the old page could not evaluate 1 + 1.
It should not be, unless you asked for it. The default reading inserts the replacement character for character. If you switch to "With $1 and $& patterns", the ECMA-262 substitution table applies and $& becomes the matched text, $1 a capture group and $$ a single dollar sign. Turning on Use regex turns that reading on with it.
Yes. Leave the replacement box empty and every match is removed. The summary and the rules list both say so before you commit.
Yes, both numbered and named. Use $1 and $2, or $<year> and $<month> for named groups, with the reading set to "With $1 and $& patterns". A group that did not participate in the match substitutes as nothing, which is what ECMA-262 specifies.
That the match is not adjacent to a letter, digit, mark or underscore on either side. That is deliberately not the \b assertion, which is a boundary between a word and a non-word character and therefore matches C++ inside C++11 while refusing café before a comma. This definition does neither.
Because those are different facts and only one of them is your fault. An unterminated character class is a typo you want to know about; a pattern that matched nothing is an answer. The previous version reported both as zero matches with no message.
Yes. Select a specific column from the dropdown, or leave it on All Columns. The rules list under the result says which column was searched, counting from one.
Not unless you turn on "Search the header row". By default the header is left alone and the tool says so, because renaming a column is a different job from cleaning the data under it.
Rows and columns are parsed and transformed in memory in your browser. No record ever reaches a server.