Search and replace with .NET regular expressions from the command line, using the same engine as your C# code.
Use them for general text search, they are faster and more widely installed. Use regex when you need the .NET flavor:
- Try a pattern on the CLI, then paste it into C# unchanged. No dialect differences.
- .NET-only features such as balancing groups, and named groups in replacements (
--replace "id=${name}"). - Lookarounds and backreferences without extra flags or a separate engine.
- One tool with the same behavior on Linux, macOS and Windows, installed via
dotnet tool.
- .NET Regular Expressions for CLI on Linux, macOS and Windows
- .NET Tool Deployment
- NuGet Package
- Permissive MIT License
Requires the .NET 10 SDK or runtime.
dotnet tool install --global cap.regex
Update to the latest version:
dotnet tool update --global cap.regex
Usage:
regex [<pattern> [<path>...]] [options]
Arguments:
<pattern> The search pattern as .NET Regular Expression (RegEx).
<path> File or directory to operate. (A directory must end with a directory separator)
Options:
-R, --replace <replace> replacement Pattern (regex)
-n, --dry-run with --replace: report what would change, but do not write any file
-d, --diff with --replace: print the changes as unified diff instead of the summary, but do not write any file (implies --dry-run)
-c, --case-sensitive enables case-sensitive behavior - btw. disables the by default enabled ignore-case option
-f, --filter <filter> wildcard based file filter, e.g. *.txt [default: *.*]
-r, --recursive progress all subdirectories
--offset-width <offset-width> output-formatting: set the count of characters used for the offset column [default: 6]
-o, --only-matching prints only the match
-m, --max-count <max-count> limit matches to the given count
-v, --verbose show additional information
-V, --version show version information
-?, -h, --help Show help and usage information
Exit code is 0 on success and 1 on errors (e.g. missing pattern, invalid regular expression or invalid option value).
Every example is self-contained: the files it starts from, the command, and the exact output. Click to expand.
Search a directory tree for a word in *.txt files
Starting point:
docs/
├── intro.txt "Hello World" / "hello again" / "Goodbye"
└── sub/
├── more.txt "Say Hello to everyone" / "nothing here"
└── skip.md "no match"
Command (--recursive walks subfolders, --filter limits the files, a directory must end with /):
$ regex --recursive --filter '*.txt' Hello docs/
Offset:0 Hello World
Offset:12 hello again
docs/intro.txt: found 2 matches
Offset:4 Say Hello to everyone
docs/sub/more.txt: found 1 match
Matching is case-insensitive by default, so hello again is found too. The offset is the character position of the match in the file. skip.md is ignored by the filter.
Case-sensitive search with --case-sensitive
notes.txt:
Hello there
hello there
Default (case-insensitive) versus --case-sensitive:
$ regex Hello notes.txt
Offset:0 Hello there
Offset:12 hello there
notes.txt: found 2 matches
$ regex --case-sensitive Hello notes.txt
Offset:0 Hello there
notes.txt: found 1 match
Print only the match with --only-matching and --max-count
names.txt:
Name:Alice
Name:Bob
Age:42
Print just the matched text instead of the whole line:
$ regex --only-matching 'Name:\w+' names.txt
Name:Alice
Name:Bob
Stop after the first match:
$ regex --max-count 1 --only-matching 'Name:\w+' names.txt
Name:Alice
Replace with a named group (rewrites the file)
names.txt before:
Name:Alice
Name:Bob
Age:42
Command. Use single quotes in bash/zsh so the shell does not expand ${name}:
$ regex 'Name:(?<name>[A-Za-z]+)' --replace 'Hello ${name}, how are you?' names.txt
names.txt: did 2 replacements
names.txt after:
Hello Alice, how are you?
Hello Bob, how are you?
Age:42
Files without matches are left untouched.
Preview a replacement with --dry-run (counts only)
--dry-run reports what would change and writes nothing. It also works across a whole tree.
docs/intro.txt contains 2 matches for hello, docs/sub/more.txt contains 1:
$ regex --recursive --filter '*.txt' --dry-run hello --replace bye docs/
docs/intro.txt: would do 2 replacements
docs/sub/more.txt: would do 1 replacement
Both files are unchanged afterwards.
Preview as unified diff with --diff, then apply it with patch
names.txt:
Name:Alice
Name:Bob
Age:42
--diff prints the changes as a unified diff and writes nothing:
$ regex 'Name:(?<name>[A-Za-z]+)' --replace 'id=${name}' --diff names.txt
--- names.txt
+++ names.txt
@@ -1,3 +1,3 @@
-Name:Alice
-Name:Bob
+id=Alice
+id=Bob
Age:42
The diff goes to stdout without colors when redirected, so you can save it, review it and apply it later with patch -p0 or git apply -p0 (use relative paths, git apply rejects absolute ones):
$ regex 'Name:(?<name>[A-Za-z]+)' --replace 'id=${name}' --diff names.txt > names.patch
$ patch -p0 < names.patch
patching file names.txt
names.txt after patch:
id=Alice
id=Bob
Age:42
See CHANGELOG.md.
