findstr and find in Windows: the regex dialect, the traps and which one to use

Every CMD one-liner that filters output ends in one of two commands. find looks for a literal string. findstr looks for a pattern. They are not the same tool with different names, they do not take the same switches, and choosing the wrong one is how a filter that looks right returns the wrong lines.

The trouble is that findstr looks like grep and is not. It has ten metacharacters, and the ones you reach for out of habit are not among them: no +, no ?, no {n,m}, no alternation, no groups. Those characters do not error, they match themselves. So a pattern that would work anywhere else quietly matches nothing, or matches far too much.

This page covers which of the two to reach for, the dialect findstr actually implements, and the handful of behaviours that cause almost every wrong result. Every match count below was measured against the same sample, so you can rerun any of them.

Applies to: Windows 10 / 11, Windows Server 2016 / 2019 / 2022 / 2025


Quick answer

Both read a file named on the command line, and both act as a filter when you pipe into them instead. Neither needs elevation.

rem literal phrase: find needs the quotes, and they are not optional
tasklist | find "svchost.exe"

rem literal phrase in findstr: /c: groups it, /l makes it literal
tasklist | findstr /l /c:"svchost.exe"

rem pattern: /r is already the default, so this line is a regular expression
findstr /r "^ERROR" C:\logs\app.log
Common mistake: findstr "SERVICE_NAME STOPPED" does not search for that phrase. Spaces separate multiple search strings, so it returns every line containing either word. Measured against the sample below: 2 lines, neither of them the phrase. findstr /c:"SERVICE_NAME STOPPED" returns 0, correctly, because no line contains it. The sc.exe guide documents where this bites hardest.

find or findstr: the decision

findstr is the bigger tool and does almost everything find does. There are still three jobs where find is the better answer, and they are all about how a batch file reads the result rather than about matching.

What you needReach forWhy
A literal phrase, nothing cleverfindNo metacharacters exist, so nothing can be misread as one
A count of matching linesfind /cfindstr has no counting switch at all
An exit code for a conditionalfind0, 1 and 2 are documented; findstr publishes none
A regular expressionfindstrfind accepts no wildcards or patterns whatsoever
A whole word, not a substringfindstrThe \< and \> word anchors
To search subdirectoriesfindstr /sfind cannot recurse
Only the names of matching filesfindstr /mfind always prints the lines
Many patterns at oncefindstr /g:Reads the pattern list from a file
To read an alternate data streamfindstrMicrosoft documents that find cannot

There is one more difference worth knowing before you trust a result. Microsoft documents that find does not recognise carriage returns, so a search phrase that happens to straddle one is never reported as a match. If you are searching a file with hard-wrapped text for a phrase of several words, that is a silent miss, and findstr is the safer tool.


The two switch lists

find has five switches and that is the whole tool.

find [/v] [/c] [/n] [/i] [/off[line]] <"string"> [[<drive>:][<path>]<filename>[...]]
SwitchWhat it does
/vDisplays all lines that do not contain the string.
/cCounts the lines that contain the string and displays the total.
/nPrecedes each line with the file’s line number.
/iMakes the search case insensitive.
/off[line]Does not skip files that have the offline attribute set.

findstr has eighteen. These are the ones that earn their place in day-to-day use; the full list is on the reference page linked at the end.

SwitchWhat it does
/c:stringTreats the whole argument as one search string, spaces included. The single most important switch on the tool. Microsoft’s table calls this “literal”, but see the hidden gem below: it groups, it does not disarm metacharacters.
/lProcesses search strings literally.
/rProcesses search strings as regular expressions. This is the default setting.
/bMatches the pattern only at the beginning of a line.
/eMatches the pattern only at the end of a line.
/xPrints lines that match exactly, end to end.
/vPrints only lines that do not contain a match.
/iIgnores case.
/nPrints the line number of each matching line.
/mPrints only the file name if a file contains a match.
/oPrints the character offset before each matching line.
/sSearches the current directory and all subdirectories.
/pSkips files with non-printable characters.
/g:fileGets search strings from the specified file.
/f:fileGets the list of files to search from the specified file.
/d:dirlistSearches the listed directories, separated by semicolons.
Note: every findstr option must come before the search string and the file name. Microsoft states this explicitly, and it is the reason findstr file.txt /i "term" behaves nothing like you expect.

The regex dialect findstr actually has

This is the section that matters. Microsoft documents exactly ten metacharacters, and that is the entire language.

MetacharacterMeaning
.Wildcard: any character
*Repeat: zero or more of the previous character or class
^Beginning of the line
$End of the line
[class]Any one character in a set
[^class]Any one character not in a set
[x-y]Any character in the range
\xEscape: literal use of a metacharacter
\<stringBeginning of a word
string\>End of a word

Read that list for what is missing. There is no +, no ?, no {n,m} quantifier, no alternation and no grouping. Those characters are not errors in a pattern: they are ordinary literals, so they match themselves and nothing else.

Everything below was measured against this fourteen-line sample.

Version 3.5 installed
Build 365 shipped
Version 3x5 beta
abb detected
ab+ literal token
cat found
dog found
cat|dog found
SERVICE_NAME: Winmgmt
        STATE              : 4  RUNNING
        STATE              : 1  STOPPED
computer room
computerised list
the computer

A dot is a wildcard, and you did not ask for one

/r is the default. Not /l. So a version number typed straight into a filter is a pattern, and the dot in it matches any character at all.

rem 3 lines: "3.5", "365" and "3x5" all match, because . is any character
findstr "3.5" sample.txt

rem 1 line: /l turns the whole search string literal
findstr /l "3.5" sample.txt

rem 1 line: so does escaping the dot
findstr "3\.5" sample.txt
Result: the same search string returns 3 lines or 1 line depending on a switch you did not type. Any time the thing you are searching for contains a dot, and IP addresses, version numbers and file names all do, either pass /l or escape it.

The metacharacters that are not there

+ and | are the two that catch people arriving from grep or PowerShell. Run the same two patterns through findstr and through a full regular expression engine and the counts diverge.

Patternfindstr returnsA full regex engine returns
ab+1 line: ab+ literal token2 lines: also abb detected
cat|dog1 line: cat|dog found3 lines: cat, dog and cat|dog

findstr read both patterns as literal text and found the one line that literally contains it. No error, no warning. The way to express “one or more” in this dialect is to write the character once and then again followed by the repeat star, and the way to express alternation is to use spaces, which brings us to the behaviour everyone meets first.

rem "one or more b" has no + here: write the first one, then repeat
findstr "abb*" sample.txt

rem alternation IS the space-separated form, which is why /c: exists
findstr "cat dog" sample.txt
Warning: never type an unquoted pipe in a pattern. findstr cat|dog is not a search, it is CMD piping findstr’s output into a command called dog. The quotes in findstr "cat|dog" are what stop that happening, and the pattern is still only ever a literal.

Word boundaries are \< and \>

There is no \b. The word anchors are angle brackets, and they are the cleanest way to stop a short search term matching inside longer words.

rem 3 lines: "computer room", "computerised list", "the computer"
findstr "computer" sample.txt

rem 2 lines: "computerised" is excluded
findstr "\<computer\>" sample.txt

Practical examples

1. Filter command output for one exact phrase

The problem: you filter a service listing for stopped services and get the whole listing back.

The solution: the phrase has a space in it, so it must be one search string rather than two. That is what /c: does, and it is the fix for most wrong output from this tool.

rem WRONG: two search strings, so every line with either word comes back
sc query state= all | findstr "SERVICE_NAME STOPPED"

rem RIGHT: one literal string
sc query state= all | findstr /c:"1  STOPPED"

2. Find which file contains a setting

The problem: a connection string is wrong somewhere under a deployment folder and you do not know which of forty files holds it.

The solution: /s recurses, /m prints file names instead of lines, and /i removes case from the question. This combination is the one worth committing to memory.

rem names of matching files only, case insensitive, whole tree
findstr /s /m /i /c:"Data Source=" C:\deploy\*.config

rem add /n and drop /m when you want to see the line and where it is
findstr /s /n /i /c:"Data Source=" C:\deploy\*.config

3. Anchor a field so a prefix cannot over-match

The problem: filtering a report for one field also returns rows whose values happen to contain the field’s name.

The solution: /b anchors the match to the start of the line, which is where a label lives and a value does not. The systeminfo guide measures this in detail on a real report: the unanchored filter returns four lines where two were meant.

rem label only, because only a label starts the line
systeminfo | findstr /b /c:"OS Name" /c:"OS Version"

rem the regex equivalent, when you need more than a prefix
systeminfo | findstr /r /c:"^OS "

4. Count without reading

The problem: you want to know how many times something appears, not what it says.

The solution: this is the job find keeps, because findstr has no counting switch. Note the second form: /v and /c together count the lines that do not match.

rem how many error lines
find /c "ERROR" C:\logs\app.log

rem how many lines are NOT error lines
find /v /c "ERROR" C:\logs\app.log

Against the fourteen-line sample above, find /c "found" returns 3 and find /v /c "found" returns 11, which add back to 14. That is the check to run whenever a count looks wrong: the two figures must sum to the line count.

5. Search for many patterns at once

The problem: you have a list of thirty error codes and need to know which appear in a log.

The solution: put one pattern per line in a file and hand it to /g:. The companion /f: takes the list of files to search the same way.

rem codes.txt holds one search string per line
findstr /g:codes.txt C:\logs\app.log

rem both lists from files, result to a report
findstr /g:codes.txt /f:filelist.txt > results.out
Note: a pattern file is also the readable way to express alternation, since the dialect has none. Thirty lines in a file do what (a|b|c) would do elsewhere, and the file can be kept under version control.

Exit codes and batch conditionals

This is the strongest reason to keep find in your vocabulary. Microsoft documents its exit codes; the findstr reference page publishes none at all. When a batch file has to branch on whether something was found, the tool with a documented contract is the one to use.

Exit codeMeaning
0The searched string was found
1Searched string not found
2Searched file not found, or an invalid switch was given

That turns a service check into one line. This is Microsoft’s own example from the reference page, and the >nul 2>&1 is what keeps the matched line from printing while still setting the code.

sc query Winmgmt | find "RUNNING" >nul 2>&1 && (echo service is started) || (echo service is stopped)

Exit code 2 is worth handling separately in anything scheduled. A typo in a path and a genuinely absent string both leave the branch untaken, but only one of them is a bug in your script.

find "ERROR" C:\logs\app.log >nul
if errorlevel 2 (echo cannot read the log) else if errorlevel 1 (echo clean) else (echo errors present)
Warning: if errorlevel N means “N or higher”, which is why the checks above run from the largest code down. Written the other way round, if errorlevel 1 would swallow the 2 case as well.

Hidden gems

/c: and /l are different axes, not alternatives

Microsoft’s parameter table calls /c: a “literal search string”, which reads as though it switches matching off. It does not. /c: decides grouping: it makes the whole argument one search string, spaces included, instead of several. /l and /r decide interpretation. They are set independently, and /r is still the default.

Microsoft’s own example on that same page proves it, because the ^ and the * inside the /c: string are plainly still doing work:

findstr /b /n /r /c:^ *FOR *.bas

So findstr /c:"3.5" still treats the dot as a wildcard. If you want a phrase and you want it literal, ask for both.

FormOne string?Metacharacters live?
findstr "a b"No, two patternsYes
findstr /c:"a b"YesYes
findstr /l /c:"a b"YesNo
findstr /l "a b"No, two literal patternsNo

A quotation mark inside a find string is tripled

find requires its search string in quotes, which leaves no obvious way to search for a quote. Microsoft’s documented answer is to double every quotation mark inside the string, which with the surrounding pair produces three in a row at each end.

find """This string contains quotation marks""" report.txt

findstr has no equivalent rule, which is another quiet argument for using it on anything with unusual characters in it.

The find on your PATH may not be Windows find

On a workstation with Git Bash, Cygwin or any Unix toolchain installed, find may resolve to the Unix utility of the same name, which takes a directory to walk rather than a string to match. It fails with a message that looks nothing like a CMD error:

/usr/bin/find: 'System Type': No such file or directory

That report came from an issue opened against Microsoft’s own WSL documentation, which told readers to run a find filter. There is no Unix command called findstr, so it cannot be shadowed this way. The systeminfo guide tells the full story, and it is the reason every example on that page uses findstr.

Combining find switches has two documented surprises

Two combinations do not compose the way they read. /c with /v gives a count of the non-matching lines, which is useful once you expect it. /c with /n silently drops /n, because a count has no line numbers to print.

CommandWhat comes back
find /c "x"Count of matching lines
find /v /c "x"Count of lines that do not match
find /c /n "x"A count. /n is ignored

findstr can read an alternate data stream; find cannot

Microsoft states plainly that find cannot read alternate data streams and names findstr, more and for /f as the tools that can. If you are looking at a file that something has attached hidden content to, that one sentence decides which tool you use.

rem the stream is named after the colon
findstr /c:"anything" "C:\temp\notes.txt:hidden"

PowerShell: Select-String

Select-String is the replacement for both, and the comparison is not close: it uses .NET regular expressions, so every construct missing from findstr is present. Microsoft’s own documentation makes the comparison explicit, describing the -Raw parameter as behaving “most similar to the Unix grep or Windows findstr.exe commands”.

TaskCMDPowerShell
Literal searchfindstr /l /c:"text"Select-String -SimpleMatch "text"
Regular expressionfindstr /r "^ERROR"Select-String -Pattern "^ERROR"
Case sensitivedefault-CaseSensitive (default is insensitive)
Invertfindstr /v-NotMatch
File names onlyfindstr /m-List
Recursefindstr /sGet-ChildItem -Recurse | Select-String
Just a yes or nofind exit code-Quiet
Lines around the matchnot available-Context 2,3

Two of those have no CMD equivalent at all and are the reason to switch. -Context gives you the lines either side of a hit, which is most of what log reading actually needs, and the default output is an object with the file, the line number and the matched text already separated.

# two lines before and three after every match, across a whole tree
Get-ChildItem C:\logs -Filter *.log -Recurse |
    Select-String -Pattern 'timeout|refused|reset' -Context 2,3

# a yes or no for a conditional, with no output to discard
if (Select-String -Path C:\logs\app.log -Pattern '^ERROR' -Quiet) {
    'errors present'
}

Note the alternation in that first pattern. Three alternatives in one expression is the thing findstr cannot express at all, and it is a fair summary of why the newer tool wins whenever PowerShell is available.


Where this matters

  • A filter returns the whole listing: almost always a space in the search string being read as two patterns, fixed by /c:.
  • A filter for an IP or a version returns neighbours: the dot is a wildcard, because /r is the default.
  • A pattern that works elsewhere returns nothing: it used +, ? or alternation, none of which exist here.
  • A scheduled batch file takes the wrong branch: it read an exit code from the tool that does not document one.
  • Hunting a setting across a deployment tree: findstr /s /m /i answers in one command what a GUI search takes minutes to do.
  • A one-liner works on your machine and not on a colleague’s: their PATH has a Unix find in front of the Windows one.

Tips and limitations

  • Spaces in a findstr search string mean OR. If the thing you are looking for contains a space, it needs /c: or it is not one search string.
  • /r is the default, so every dot, asterisk, caret, dollar and square bracket in a plain search string is already a metacharacter. Use /l for literal text: /c: only groups the argument, it does not disarm it.
  • The dialect has ten metacharacters and no quantifiers beyond *. Anything needing +, ?, {n,m}, alternation or groups belongs in Select-String.
  • All findstr switches must precede the search string and the file name.
  • find does not recognise carriage returns, so a multi-word phrase interrupted by one is never reported. findstr does not share this limitation.
  • find documents exit codes 0, 1 and 2; the findstr reference documents none. Branch on find.
  • Neither tool is a parser. Once you are extracting fields rather than selecting lines, for /f or PowerShell is the right next step.

Official documentation


Related tools


Related guides