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
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 need | Reach for | Why |
|---|---|---|
| A literal phrase, nothing clever | find | No metacharacters exist, so nothing can be misread as one |
| A count of matching lines | find /c | findstr has no counting switch at all |
| An exit code for a conditional | find | 0, 1 and 2 are documented; findstr publishes none |
| A regular expression | findstr | find accepts no wildcards or patterns whatsoever |
| A whole word, not a substring | findstr | The \< and \> word anchors |
| To search subdirectories | findstr /s | find cannot recurse |
| Only the names of matching files | findstr /m | find always prints the lines |
| Many patterns at once | findstr /g: | Reads the pattern list from a file |
| To read an alternate data stream | findstr | Microsoft 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>[...]]
| Switch | What it does |
|---|---|
/v | Displays all lines that do not contain the string. |
/c | Counts the lines that contain the string and displays the total. |
/n | Precedes each line with the file’s line number. |
/i | Makes 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.
| Switch | What it does |
|---|---|
/c:string | Treats 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. |
/l | Processes search strings literally. |
/r | Processes search strings as regular expressions. This is the default setting. |
/b | Matches the pattern only at the beginning of a line. |
/e | Matches the pattern only at the end of a line. |
/x | Prints lines that match exactly, end to end. |
/v | Prints only lines that do not contain a match. |
/i | Ignores case. |
/n | Prints the line number of each matching line. |
/m | Prints only the file name if a file contains a match. |
/o | Prints the character offset before each matching line. |
/s | Searches the current directory and all subdirectories. |
/p | Skips files with non-printable characters. |
/g:file | Gets search strings from the specified file. |
/f:file | Gets the list of files to search from the specified file. |
/d:dirlist | Searches the listed directories, separated by semicolons. |
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.
| Metacharacter | Meaning |
|---|---|
. | 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 |
\x | Escape: literal use of a metacharacter |
\<string | Beginning 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
/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.
| Pattern | findstr returns | A full regex engine returns |
|---|---|---|
ab+ | 1 line: ab+ literal token | 2 lines: also abb detected |
cat|dog | 1 line: cat|dog found | 3 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
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
(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 code | Meaning |
|---|---|
0 | The searched string was found |
1 | Searched string not found |
2 | Searched 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)
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.
| Form | One string? | Metacharacters live? |
|---|---|---|
findstr "a b" | No, two patterns | Yes |
findstr /c:"a b" | Yes | Yes |
findstr /l /c:"a b" | Yes | No |
findstr /l "a b" | No, two literal patterns | No |
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.
| Command | What 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”.
| Task | CMD | PowerShell |
|---|---|---|
| Literal search | findstr /l /c:"text" | Select-String -SimpleMatch "text" |
| Regular expression | findstr /r "^ERROR" | Select-String -Pattern "^ERROR" |
| Case sensitive | default | -CaseSensitive (default is insensitive) |
| Invert | findstr /v | -NotMatch |
| File names only | findstr /m | -List |
| Recurse | findstr /s | Get-ChildItem -Recurse | Select-String |
| Just a yes or no | find exit code | -Quiet |
| Lines around the match | not 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
/ris 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 /ianswers 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
PATHhas a Unixfindin front of the Windows one.
Tips and limitations
- Spaces in a
findstrsearch string mean OR. If the thing you are looking for contains a space, it needs/c:or it is not one search string. /ris the default, so every dot, asterisk, caret, dollar and square bracket in a plain search string is already a metacharacter. Use/lfor 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 inSelect-String. - All
findstrswitches must precede the search string and the file name. finddoes not recognise carriage returns, so a multi-word phrase interrupted by one is never reported.findstrdoes not share this limitation.finddocuments exit codes 0, 1 and 2; thefindstrreference documents none. Branch onfind.- Neither tool is a parser. Once you are extracting fields rather than selecting lines,
for /for PowerShell is the right next step.
Official documentation
- findstr: Windows Commands | Microsoft Learn
- find: Windows Commands | Microsoft Learn
- Select-String | Microsoft Learn
- about_Regular_Expressions | Microsoft Learn
Related tools
- Event log analyzer: searching logs without writing the filter by hand.
- System tools: the rest of the local diagnostic utilities on this site.
Related guides
- sc.exe for Windows services: where the spaces-are-OR trap does the most damage, worked through on real service output.
- systeminfo in Windows: the anchoring measured line by line, plus the full story of the shadowed
find. - route print and route add on Windows: a substring over-match on an IP address, and the anchored form that fixes it.
- arp and the ARP cache:
/iin practice, because MAC addresses arrive in whichever case the source used. - Windows command aliases with doskey: how to stop retyping these filters altogether.
- Windows Command Line (CMD) Cheat Sheet: the one-line forms alongside the rest of the set.