Question
I have a string and need to scan for every occurrence of "foo", then read all the text that follows it until a second " character. I thought I would need to iterate through the string character by character to search for it, because I believed Rust strings did not have a contains function.
How can this be done in Rust?
Update: Rust's &str does provide contains() and find() methods.
Short Answer
By the end of this page, you will understand how string iteration works in Rust, why strings are not indexed by character, and when iterating with chars() is useful. You will also learn that many text-search tasks are better solved with built-in &str methods such as contains(), find(), match_indices(), and slicing carefully at valid UTF-8 boundaries.
Concept
Rust strings are a little different from strings in many other languages.
A Rust string slice, &str, stores UTF-8 text. UTF-8 uses a variable number of bytes per character, so Rust does not allow direct indexing like s[0] for characters. That is important because a single visible character may take more than one byte.
Because of that, Rust gives you several safe ways to work with strings:
chars()to iterate over Unicode scalar valuesbytes()to iterate over raw bytescontains()to check whether text exists inside a stringfind()to get the byte position of a matchmatch_indices()to find all matches- string slicing like
&s[start..end], but only at valid UTF-8 boundaries
For the specific problem in the question, iterating character by character is often not the best first approach. If you want to find every occurrence of a substring like "foo", Rust already provides substring search methods.
Why this matters in real programming:
- Text parsing is common in log files, config files, CSV-like formats, and templates.
- Using built-in string methods usually makes code shorter and less error-prone.
- Understanding UTF-8 boundaries helps you avoid panics when slicing strings.
So the key lesson is:
- Use
chars()when you truly need per-character logic. - Use
find(),contains(),split(), ormatch_indices()when you are searching for substrings or patterns.
Mental Model
Think of a Rust string like a box of encoded text bytes, not a simple row of fixed-size letters.
If you want to inspect each letter one by one, you ask Rust to decode the string using chars().
If you want to search for a word like "foo", you usually do not need to open the box one character at a time. Instead, you use built-in search tools like find() or match_indices() that are designed for that job.
So:
chars()= reading the text one character at a timebytes()= inspecting the raw storagefind()/contains()= asking Rust to search the text for you
Syntax and Examples
Iterating over characters
fn main() {
let text = "foo \"hello\" bar";
for ch in text.chars() {
println!("{}", ch);
}
}
This prints each character in the string, one at a time.
Checking whether a string contains text
fn main() {
let text = "foo \"hello\" bar";
if text.contains("foo") {
println!("Found foo");
}
}
Use contains() when you only need to know whether the substring exists.
Finding the first occurrence
fn main() {
let text = "abc foo \"hello\" xyz";
if let Some(index) = text.find("foo") {
println!("foo starts at byte index {}", index);
}
}
find() returns Option<usize>:
Some(index)if foundNoneif not found
Finding every occurrence of foo
fn main() {
let text = "foo \"one\" and foo \"two\"";
for (index, _) in text.match_indices("foo") {
println!("Found foo at {}", index);
}
}
This is a better fit than manually scanning characters when searching for repeated substrings.
Extracting text after foo until the next "
fn main() {
let text = "foo \"one\" and foo \"two\"";
for (start, _) in text.match_indices("foo \"") {
let content_start = start + "foo \"".len();
if let Some(end_offset) = text[content_start..].find('"') {
let content_end = content_start + end_offset;
let value = &text[content_start..content_end];
println!("{}", value);
}
}
}
This prints:
one
two
Here is what happens:
match_indices("foo \"")finds eachfoo "content_startmoves to the first character after the quotefind('"')searches for the closing quote- slicing extracts the text between the quotes
Step by Step Execution
Consider this example:
fn main() {
let text = "foo \"cat\" and foo \"dog\"";
for (start, _) in text.match_indices("foo \"") {
let content_start = start + "foo \"".len();
if let Some(end_offset) = text[content_start..].find('"') {
let content_end = content_start + end_offset;
let value = &text[content_start..content_end];
println!("Found: {}", value);
}
}
}
Step-by-step
-
textis set to:foo "cat" and foo "dog" -
match_indices("foo \"")looks for every occurrence of the exact substringfoo ". -
On the first match:
startis0"foo \"".len()is5content_start = 0 + 5 = 5
-
text[content_start..]is now:cat" and foo "dog" -
.find('"')finds the next quote inside that sliced text.- It returns the offset of the closing quote relative to the slice.
-
content_end = content_start + end_offset -
&text[content_start..content_end]extractscat -
The loop continues for the second match and extracts
dog
Important detail
The indexes returned by find() and match_indices() are byte indexes, not character positions. That is normal in Rust string handling.
Real World Use Cases
String iteration and substring searching show up in many real programs.
Parsing quoted values
You might scan text like:
name="Alice" role="admin"
and extract the value inside quotes.
Processing log files
Applications often search logs for markers such as:
ERRORWARNuser="..."
Reading configuration-like text
Simple parsers often need to locate prefixes and capture the text that follows.
Token scanning
A script may walk through characters when checking:
- punctuation
- delimiters
- quoted strings
- escaped characters
Input validation
You may search user input for forbidden words, expected prefixes, or structured patterns before storing data.
Real Codebase Usage
In real Rust codebases, developers usually prefer built-in string operations over manual character scanning when possible.
Common patterns
Use search methods first
If you are looking for a fixed substring:
contains()for yes/no checksfind()for the first matchrfind()for the last matchmatch_indices()for all matches
Use guard clauses
fn extract_after_foo(text: &str) -> Option<&str> {
let start = text.find("foo \"")? + "foo \"".len();
let end = text[start..].find('"')? + start;
Some(&text[start..end])
}
This uses ? with Option for clean early returns.
Avoid unnecessary allocation
In many cases, return &str slices instead of building new String values.
Iterate only when parsing rules are character-based
If you need to handle things like:
- escaped quotes
- nested delimiters
- custom tokenization
then a chars() loop or a small state machine can make sense.
Validate before slicing
Developers often search first, then slice using the byte indexes returned by string methods. This is safer than trying to guess positions manually.
Common Mistakes
1. Trying to index a string like an array
Broken code:
let s = "hello";
let first = s[0];
This does not compile because Rust strings are UTF-8 and are not directly indexable by character.
Avoid this by using:
s.chars().next()for the first characterfind()for substring search
2. Using character iteration for substring search when a string method is simpler
Broken approach:
for ch in text.chars() {
// manually trying to detect "foo"
}
This is possible, but often more complicated than needed.
Better:
if text.contains("foo") {
println!("Found it");
}
3. Forgetting that indexes are byte indexes
let s = "héllo";
println!("{:?}", s.find("é"));
The result is a byte position, not a character number.
4. Slicing at invalid UTF-8 boundaries
Broken code:
let s = "héllo";
let part = &s[1..2];
This may panic because byte indexes 1..2 may cut through a multibyte character.
Avoid this by using indexes returned from valid string operations, or by iterating over char_indices().
5. Assuming find() returns a number every time
Broken code:
let index = text.find("foo").unwrap();
This will panic if foo is missing.
Safer:
if let Some(index) = text.find("foo") {
println!("{}", index);
}
6. Confusing String and &str
Stringowns text&strborrows text
Most string search methods are available on &str, and String can be borrowed as &str.
Comparisons
| Concept | Best for | Returns | Notes |
|---|---|---|---|
chars() | Per-character processing | char iterator | Good for parsing character rules |
bytes() | Raw byte processing | u8 iterator | Useful for low-level work |
contains() | Check whether text exists | bool | Simplest existence check |
find() | First occurrence | Option<usize> | Returns byte index |
match_indices() | All occurrences | Iterator of (usize, &str) | Great for repeated matches |
split() | Breaking text into parts | Iterator | Useful when separators are clear |
chars() vs find()
- Use
chars()when logic depends on each character. - Use
find()when searching for a known substring.
find() vs match_indices()
find()gets the first match only.match_indices()gets every match.
String vs &str
| Type | Ownership | Typical use |
|---|---|---|
String | Owns the text | Store and modify text |
&str | Borrows the text | Read/search text without ownership |
Cheat Sheet
Quick reference
Iterate over characters
for ch in text.chars() {
println!("{}", ch);
}
Iterate with positions
for (i, ch) in text.char_indices() {
println!("{}: {}", i, ch);
}
Check for a substring
text.contains("foo")
Find first match
text.find("foo")
Find all matches
text.match_indices("foo")
Safe extraction pattern
if let Some(start) = text.find("foo \"") {
let content_start = start + "foo \"".len();
if let Some(end_offset) = text[content_start..].find('"') {
let end = content_start + end_offset;
let value = &text[content_start..end];
println!("{}", value);
}
}
Rules to remember
- Rust strings are UTF-8.
- You cannot index strings with
s[0]. find()returns a byte index.- Slice only on valid UTF-8 boundaries.
- Prefer built-in string methods over manual scanning when possible.
FAQ
Does Rust really have a contains() method for strings?
Yes. &str has a contains() method, and it can be used through String as well by borrowing it as a string slice.
Why can't I use s[0] on a Rust string?
Because Rust strings are UTF-8, and one character may use multiple bytes. Direct indexing would be ambiguous and potentially unsafe.
When should I use chars() in Rust?
Use chars() when your logic depends on processing one character at a time, such as counting letters or parsing delimiters.
What does find() return in Rust?
It returns Option<usize>, where the usize is the byte index of the first match.
How do I find every occurrence of a substring in Rust?
Use match_indices() to iterate over all matches and their positions.
Is find() enough for parsing quoted text?
For simple cases, yes. For more complex formats with escapes or nested structures, you may need a more detailed parser.
Should I use String or &str for searching?
Usually &str is enough for searching. String is needed when you own or modify the text.
Mini Project
Description
Build a small Rust program that scans a line of text and extracts every value that appears after foo " and before the next ". This demonstrates practical string searching, repeated matching, and safe slicing with &str.
Goal
Write a Rust program that collects and prints all quoted values following foo in a string.
Requirements
- Read from a hardcoded input string containing multiple
foo "..."segments. - Find every occurrence of
foo ". - Extract the text until the next closing
". - Print each extracted value.
- Skip incomplete matches that do not have a closing quote.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.