Detailed feature comparisons between msr/nin and common CLI tools, plus irreplaceable features unique to msr/nin.
This content was split from Practical Use Cases and Tool Comparisons for focused reading.
Looking for use cases and examples? See Practical Use Cases and Tool Comparisons.
Looking for tool references? See msr User Guide, nin User Guide, vscode-msr User Guide.
- Tool Comparisons
Looking for regex syntax basics? See msr and nin Shared Reference — Regex Syntax for supported syntax, escape rules, and cross-platform behavior.
Detailed regex feature comparison between msr/nin and common CLI tools (grep, ripgrep).
Quick summary: msr/nin use Boost regex (PCRE-compatible with extensions) — support 22 of 23 features below. ripgrep (rg) default mode supports 12/23 (rg 13) or 13/23 (rg ≥14);
--pcre2enables 22/23. GNU grep-Psupports ~16/23 (varies by version).
| Feature | Syntax | msr/nin | rg | rg --pcre2 | grep -P | Notes |
|---|---|---|---|---|---|---|
| Basic capture groups | (\w+) |
✅ | ✅ | ✅ | ✅ | Standard capture groups |
| Character classes | [a-z] |
✅ | ✅ | ✅ | ✅ | Standard character classes |
| Alternation | a|b |
✅ | ✅ | ✅ | ✅ | Either-or matching |
| Non-greedy quantifiers | a.*?b |
✅ | ✅ | ✅ | ✅ | Minimal matching |
| Positive lookahead | foo(?=bar) |
✅ | ❌ | ✅ | ✅ | Match foo only if followed by bar |
| Negative lookahead | foo(?!bar) |
✅ | ❌ | ✅ | ✅ | Match foo only if NOT followed by bar |
| Positive lookbehind | (?<=foo)bar |
✅ | ❌ | ✅ | ✅ | Match bar only if preceded by foo |
| Negative lookbehind | (?<!foo)bar |
✅ | ❌ | ✅ | ✅ | Match bar only if NOT preceded by foo |
| Named groups (PCRE) | (?<name>\w+) |
✅ | ✅ | PCRE/.NET-style; rg ≥14 supports, rg 13 does not; grep -P may vary | ||
| Named groups (Python) | (?P<name>\w+) |
❌ | ✅ | ✅ | Python-style; msr/nin reject with exit=-1 | |
| Named backreferences | \k<name> |
✅ | ❌ | ✅ | ❌ | Reference named capture group |
| Backreferences | (\w+)\s+\1 |
✅ | ❌ | ✅ | grep -P support varies by version | |
| Word boundaries | \bword\b |
✅ | ✅ | ✅ | ✅ | Word boundary assertions |
| Possessive quantifiers | \w++ |
✅ | ✅ | ✅ | ✅ | No backtracking |
| Atomic groups | (?>atomic) |
✅ | ❌ | ✅ | ✅ | No backtracking |
| Conditional patterns | (?(1)yes|no) |
✅ | ❌ | ✅ | ✅ | Conditional backreference |
| Unicode properties | \p{L} |
✅ | ✅ | ✅ | grep -P may not support; rg supports by default | |
| Nested quantifiers | (a+)+ |
✅ | ✅ | ✅ | ✅ | Nested repetition |
| Anchors | ^line$ |
✅ | ✅ | ✅ | ✅ | Start/end of line |
| Inline modifiers | (?i), (?m), (?s) |
✅ | ✅ | ✅ | ✅ | In-pattern case/multiline/dotall |
| Comments | (?#comment) |
✅ | ❌ | ✅ | ✅ | Embedded regex comments |
| Recursive patterns | (?R) |
✅ | ❌ | ✅ | ❌ | Recursive matching (e.g., nested parens) |
| Branch reset | `(? | ...)` | ✅ | ❌ | ✅ | ❌ |
Legend: ✅ = Supported | ❌ = Not supported |
Supported Features Summary:
| Tool | Default Mode | Extended Mode | Total Features |
|---|---|---|---|
| msr/nin | 22/23 | — | 22 (no Python-style named groups) |
| rg | 12-13/23 | 22/23 (--pcre2) | 12 (rg 13) or 13 (rg ≥14) default, 22 with PCRE2 |
| grep | — | 15-17/23 (-P) | ~16 (varies by version) |
Note: rg (ripgrep) default mode uses Rust regex which lacks lookaround/backreferences/recursive patterns. rg ≥14 adds
(?<name>...)named group support in default mode (rg 13 only supports(?P<name>...)). Add--pcre2for full PCRE2 support. grep-Puses PCRE but support varies by platform/version (tested: GNU grep 3.0 on Windows — no named backreferences, no recursive patterns, no branch reset).
| Feature | grep | ripgrep (rg) | ag | msr |
|---|---|---|---|---|
| Regex search | ✅ | ✅ | ✅ | ✅ |
| Recursive search | ✅ -r |
✅ (default) | ✅ (default) | ✅ -rp |
| Case-insensitive | ✅ -i |
✅ -i |
✅ -i |
✅ -i |
| File type filter | ❌ (use --include) |
✅ --type |
✅ --type |
✅ -f regex |
| Text replacement | ❌ | ✅ --replace (stdout only) |
❌ | ✅ -t -o -R (in-place) |
| Preview before replace | ❌ | ❌ (no in-place) | ❌ | ✅ (default — no -R) |
| In-place replace with backup | ❌ | ❌ | ❌ | ✅ -RK |
| Block matching | ❌ | ❌ | ❌ | ✅ -b -Q |
| Return value = match count | ❌ (0/1) | ❌ (0/1) | ❌ (0/1) | ✅ actual count |
| Global fast exit | ❌ | ❌ | ❌ | ✅ -H N -J |
| Execute output as commands | ❌ | ❌ | ❌ | ✅ -X |
| File time/size filter | ❌ | --max-filesize only |
❌ | ✅ --w1/--w2/--s1/--s2 |
| Multi-file time sort | ❌ | ❌ | ❌ | ✅ -F |
| Numeric statistics | ❌ | ❌ | ❌ | ✅ -s "" -n |
| Cross-platform consistent | ❌ | ✅ | ✅ | ✅ |
| Skip-write if unchanged | N/A | N/A | N/A | ✅ |
| Line ending behavior | N/A | N/A | N/A | Uses system native (CRLF on Windows, LF on Linux/macOS) |
Key difference: Return value semantics
# grep/ripgrep: 0 = found, 1 = not found, 2 = error
grep -r "pattern" . && echo "found" # 0 means "found something"
# msr: return value = actual MATCH COUNT
msr -rp . -t "\bTargetSymbol\b" -H 0
# Returns: 0 = nothing found, 42 = found 42 matches, -1 = error
# This enables: if ($LASTEXITCODE -gt 10) { "Too many matches!" }Key difference: Global fast exit (-H N -J)
# ripgrep: can limit output lines, but still scans remaining files
rg "pattern" --max-count 3 # Limits per file, still opens all files
# msr: truly stops after N total matches across all files
msr -rp . -f "\.log$" -t "\bCRITICAL\b" -H 3 -J
# Scans first 3 matches then exits immediately — critical for 100GB+ log dirsBenchmarks: msr vs ripgrep vs grep
📊 Detailed benchmark data and optimization strategies are available in Performance Tuning & Benchmark Summary.
Performance summary (Roslyn repo, ~20K files, ~14K C# files, keyword TypeArgumentListSyntax):
| Category | Winner | Runner-up | Notes |
|---|---|---|---|
| Full scan (macOS) | rg (1.72s) | grep (4.70s) | rg is multi-threaded; msr ~7.4s |
| Full scan (Windows) | rg (12.66s) | find (14.22s) | Comparable; gfind 16.15s, grep 22.69s |
| Definition search (macOS) | msr (20.5s) | rg pipe (27.3s) | msr 25% faster — single-pass --nt |
| Path-scoped (macOS) | msr --sp (0.38s) | rg -g (0.6-1.3s) | msr has richer path filters (--sp/-d/-k combos) |
| Fast exit (macOS) | msr (67ms) | rg (86ms) | msr -H 1 -J beats rg SIGPIPE |
| Fast exit (Windows) | msr (976ms) | rg (12,710ms) | msr 13x faster — only tool with true global exit |
Key insights:
- Path filter (
--sp/-d) is the biggest optimization: up to 19.5x speedup on macOS; combined with-d+-kup to 22.5x- Fast exit (
-H N -J): On Windows, msr is 13x faster than rg (rg's-mis per-file only). On macOS, msr is also fastest (67ms vs 86ms)- Definition search: msr handles
-tmatch +--ntexclusion in a single pass; rg needsrg | grep -vpipe — msr 25% faster- msr vs rg tradeoff: rg is faster for raw full-text scan (multi-threaded); msr excels at path-scoped, definition, fast-exit, and replacement tasks
Optimization parameters reference:
-d(directory name regex match),--sp(path must contain ALL texts, AND logic),--xp(exclude if path contains ANY text, OR logic),--nf(safe filename regex exclude),--pp(safe full-path regex include),--nd/--np(regex exclusion, but may conflict with alias-built defaults),-f(filename pattern),-x(plain text AND filter),-H N -J(global fast exit after N matches). See Performance Tuning for the optimization decision tree and parameter speedup reference.
Windows path separator note (practical): see msr and nin Shared Reference — Path Separator Compatibility on Windows. findstr may still parse / as an option prefix, so forward-slash file paths are not consistently reliable there.
Why ripgrep can't replace msr:
- No in-place replacement — rg outputs to stdout only; msr writes files with
-R - No block matching — rg can't extract multi-line config sections
- No command execution — rg can't batch-execute transformed output
- Limited file filtering — rg has
--max-filesize(max size only) but no time range filtering (--w1/--w2), no min size filtering (--s1), and no--sortby time with range filter - Return value is boolean — rg can't report exact match count
- No global fast exit — rg's
-mis per-file only; msr's-H -Jstops globally
| Feature | sed | perl | msr |
|---|---|---|---|
| Regex replace | ✅ | ✅ | ✅ |
| In-place replace | ✅ -i |
✅ -pi -e |
✅ -R |
| Preview mode (default) | ❌ | ❌ | ✅ (no -R = preview) |
| Backup before replace | ✅ -i.bak |
✅ -i.bak |
✅ -K (timestamp-named) |
| Skip-write if unchanged | ❌ | ❌ | ✅ automatic |
| Line ending behavior | Converts to system native | Converts to system native | Converts to system native |
| Cross-platform consistent | ✅ | ✅ | |
| Multi-file recursive | ❌ (needs find) |
❌ (needs find) |
✅ -rp |
Two engineering safety features unique to msr:
-
Skip-write if unchanged: When
-Rproduces the same content as the original, msr does NOT write the file and does NOT updatemtime. This prevents falsegit statuschanges, unnecessary rebuilds, and spurious rsync transfers. -
Line ending behavior: Both msr and sed write using the system's native line ending style — msr uses CRLF on Windows and LF on Linux/macOS. This means:
# sed on Linux converts CRLF to LF:
sed -i 's/old/new/' windows-file.txt
# Result: all \r\n → \n (converted to system native LF)
# msr on Windows converts LF to CRLF:
msr -p unix-file.txt -t "\bold\b" -o "new" -R
# Result: all \n → \r\n (converted to system native CRLF)
# Both tools write using the system's native line ending style, NOT preserving the original.
# Be cautious when cross-platform files have mixed or non-native line endings.Cross-platform issues with sed:
# BSD sed (macOS) requires backup extension with -i:
sed -i '.bak' 's/old/new/' file.txt # macOS
sed -i 's/old/new/' file.txt # Linux (GNU)
# These are incompatible! Scripts break when moving between platforms.
# msr: identical command on all platforms
msr -p file.txt -t "\bOldSymbol\b" -o "NewSymbol" -RK # Works everywhere| Feature | awk | msr (+ nin) |
|---|---|---|
| Pattern matching | ✅ | ✅ |
| Column processing | ✅ native | |
| Text replacement | ✅ gsub() |
✅ -t -o |
| Numeric statistics | ✅ built-in P05-P99.999 | |
| Cross-platform | ✅ same binary | |
| Frequency distribution | ✅ nin -pd |
Statistics comparison:
# awk: manual P90 calculation (requires sorting, indexing)
awk '{a[NR]=$1} END{asort(a); print a[int(NR*0.9)]}' data.txt
# msr: built-in comprehensive statistics in one command
msr -p data.txt -t "(\d+\.?\d*)" -s "" -n -H 0 -C
# Output: Count, Sum, Median, Average, P05, P10, ..., P90, P95, P99, P99.9, P99.99, P99.999
# Plus: Variance, StandardDeviation, Mode, MinValue, MaxValue| Feature | find + xargs | msr |
|---|---|---|
| Recursive file listing | ✅ | ✅ -rp . -l |
| File name filter | ✅ -name |
✅ -f regex |
| File time filter | ✅ -mtime |
✅ --w1/--w2 |
| File size filter | ✅ -size |
✅ --s1/--s2 |
| Content search | ❌ (needs grep) | ✅ built-in |
| Batch execution | ✅ xargs |
✅ -X |
| Fail-fast execution | ❌ | ✅ -X -V ne0 |
| Cross-platform | ✅ |
# find + xargs: multi-command pipeline
find . -name "*.cs" -mtime -7 | xargs grep -l "pattern"
# msr: single command
msr -rp . -f "\.cs$" --w1 7d -t "\bTargetSymbol\b" -l| Feature | Select-String | msr |
|---|---|---|
| Regex search | ✅ | ✅ |
| Recursive | ✅ -Recurse |
✅ -rp |
| Case-insensitive | ✅ (default) | ✅ -i |
| Replace | ❌ | ✅ -t -o -R |
| Performance | ✅ 2X-15X faster | |
| Context lines | ✅ -Context |
✅ -U -D |
| Block matching | ❌ | ✅ -b -Q |
| Cross-platform | ✅ All platforms |
| Feature | comm | uniq | sort | nin |
|---|---|---|---|---|
| Set difference | ✅ | ❌ | ❌ | ✅ |
| Set intersection | ✅ | ❌ | ❌ | ✅ -m |
| Unique/dedup | ❌ | ✅ | ✅ -u |
✅ -u |
| Frequency count | ❌ | ✅ -c |
❌ | ✅ -pd |
| Pre-sorting required | ✅ mandatory | ✅ mandatory | N/A | ❌ not needed |
| Preserve original order | ❌ | ❌ | ❌ | ✅ (default) |
| Regex key extraction | ❌ | ❌ | ❌ | ✅ capture groups |
| Different patterns per file | ❌ | N/A | N/A | ✅ two regex args |
| Percentage output | ❌ | ❌ | ❌ | ✅ -p |
| Cumulative analysis | ❌ | ❌ | ❌ | ✅ --sum |
| Threshold filtering | ❌ | ❌ | ❌ | ✅ -K P / -k N |
| Structure-preserving filter | ❌ | ❌ | ❌ | ✅ -wn |
Key differences:
# comm: requires both files pre-sorted, no regex
sort file1.txt > /tmp/s1 && sort file2.txt > /tmp/s2 && comm -23 /tmp/s1 /tmp/s2
# nin: no pre-sorting, supports regex keys, preserves order
nin file1.txt file2.txt "^(\w+)" -u| Feature | awk | miller (mlr) | nin |
|---|---|---|---|
| Set difference | ❌ | ✅ native | |
| Frequency count | ✅ | ✅ -pd |
|
| Cumulative totals | ❌ | ❌ | ✅ --sum |
| Pareto analysis | ❌ | ❌ | ✅ -pd --sum -K |
| Threshold filtering | ❌ | ❌ | ✅ -K P / -k N |
| Format-agnostic | ✅ | ❌ (structured) | ✅ |
| Learning curve | Steep | Moderate | Low |
Concept analogy table (nin/msr ↔ SQL/Kusto):
| SQL / Kusto | nin / msr Equivalent |
|---|---|
SELECT DISTINCT col FROM table |
nin file nul "^(\S+)" -u |
GROUP BY col ORDER BY COUNT(*) DESC |
nin file nul "^(\S+)" -pd |
WHERE col NOT IN (SELECT ...) |
nin file1 file2 "(regex)" |
INNER JOIN ON key |
nin file1 file2 "(regex)" -m |
HAVING COUNT(*) >= N |
nin file nul "(regex)" -pd -k N |
TOP N |
nin file nul "(regex)" -pd -H N |
PERCENTILE_CONT(0.99) |
msr -p file -t "(\d+)" -s "" -n -H 0 -C (P99 in output) |
vscode-msr provides two sets of search aliases:
find-xxx: Uses-rp .(recursive directory traversal)gfind-xxx: Uses-w git-paths-file(pre-cached git file list)
| Scenario | find-xxx | gfind-xxx | Recommendation |
|---|---|---|---|
| Small repos (<1K files) | Baseline | ~1.5-2x faster | Use gfind-xxx |
| Large repos (20K+ files) | ~Same speed | Baseline | Either works |
| Medium repos (5K-10K files) | Equivalent | Equivalent | Either works |
Reference search (-ref) |
✅ Correct | ✅ Correct | Either works |
Definition search (-def) |
Use -ref -x instead |
Performance Test Results:
| Repository | Files | Search | find-xxx time | gfind-xxx time | Speedup |
|---|---|---|---|---|---|
| vscode-msr | 75 | -t "searchText" |
~2,100ms | ~880ms | ~2.4x |
| roslyn | 20,575 | -t "SyntaxNode" |
~14,000ms | ~14,400ms | ~1.0x |
| django | 7,021 | -t "Model" |
~3,400ms | ~3,400ms | ~1.0x |
| express | 213 | -t "Router" |
~880ms | ~880ms | ~1.0x |
Note: Speedup = find-time / gfind-time (values >1 mean gfind is faster). For medium-to-large repos, the difference is within measurement noise (~±5%). See gfind vs find benchmark data for more data.
When to use each:
# Small repos: gfind-xxx is faster
gfind-ts -t "\bTargetSymbol\b" # Uses cached git file list
# Large repos: find-xxx is more reliable
find-cs -t "\bTargetSymbol\b" # Directory traversal scales better
# Definition search: use -ref with -x (more reliable than -def)
gfind-py-ref MyClass -x class # Reliable: find "class MyClass"
gfind-cs-ref TargetService -x class -d Services # With directory filter
gfind-py -t "class\s+MyClass\b" # Or use base alias with custom regex
# gfind-py-def MyClass # May be slow or return 0 matchesTechnical note: The
gfind-xxxaliases read file lists fromgit ls-filescached output. For small repos, this avoids repeated file system traversal. For large repos, the overhead of parsing the cached list may exceed direct traversal time.
⚠️ -defalias reliability: The-defaliases use complex--ntexclusion regex that can be slow or fail on repos with long lines (2000+ characters). Recommended: Use-refwith-x "class"or-x "function", or use base alias with custom-tregex. Add-d/--sp/--xpto narrow scope.
These capabilities have no equivalent in commonly available CLI tools:
Parser-agnostic multi-line extraction that works on any text format — Helm templates, JSONC, multi-document YAML, incomplete fragments, even GB-scale files:
# No other single tool can do this:
msr -rp . -f "\.yaml$" -b "^apiVersion:" -Q "" -y -t "image:" -a
# Extracts complete K8s manifest blocks containing specific imagesEnables quantitative scripting impossible with 0/1 boolean returns:
# "Alert if more than 100 errors in this file"
msr -p app.log -x "ERROR" -H 0
if ($LASTEXITCODE -gt 100) { Send-Alert "Too many errors: $LASTEXITCODE" }
⚠️ Cross-platform caveat: On Linux/macOS, exit codes are 8-bit (max 255); on some MinGW, 7-bit (max 127). A count of 256 wraps to 0. For threshold checks that may exceed these limits, use--exit gt255-to-255(Linux/macOS) or--exit gt127-to-127(MinGW) to cap values. Existence checks (-H 1 -J, return 0 or 1) are always safe. On Windows, no truncation occurs.
msr does not write the file if replacement produces identical content — preserving mtime and avoiding false dirty states in git status and unnecessary rebuilds. No extra flag required.
Truly stops scanning after N matches across ALL files — critical for searching 100GB+ log directories:
msr -rp /var/log -f "\.log$" -t "\bCRITICAL\b" -H 1 -J
# Finds first CRITICAL and exits — doesn't scan remaining 50,000 log filesP50 through P99.999 in a single command with no scripting:
msr -p perf.log -t "latency_ms=(\d+\.?\d*)" -s "" -n -H 0 -CAutomated 80/20 analysis with data-driven threshold:
nin errors.log nul "(\w+Exception)" -pd --sum -K 5.0
# Automatically shows only significant error types (≥5% each) with cumulative coverageRelated documentation in this project:
- Practical Use Cases and Tool Comparisons — real-world examples and industry applications
- msr User Guide — comprehensive msr documentation
- nin User Guide — comprehensive nin documentation
- vscode-msr User Guide — alias usage for humans
- Performance Tuning & Benchmark Summary — optimization strategies and cross-platform benchmarks
- AI Agent Usage Guide — AI agent integration patterns