Question

How to Iterate Over a String by Character in Rust

ruststringiterator

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 values
  • bytes() to iterate over raw bytes
  • contains() to check whether text exists inside a string
  • find() to get the byte position of a match
  • match_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(), or match_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 time
  • bytes() = inspecting the raw storage
  • find() / contains() = asking Rust to search the text for you
Take Quiz

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 found
  • None if 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 each foo "
  • content_start moves to the first character after the quote
  • find('"') 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

  1. text is set to:

    foo "cat" and foo "dog"
    
  2. match_indices("foo \"") looks for every occurrence of the exact substring foo ".

  3. On the first match:

    • start is 0
    • "foo \"".len() is 5
    • content_start = 0 + 5 = 5
  4. text[content_start..] is now:

    cat" and foo "dog"
    
  5. .find('"') finds the next quote inside that sliced text.

    • It returns the offset of the closing quote relative to the slice.
  6. content_end = content_start + end_offset

  7. &text[content_start..content_end] extracts cat

  8. 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:

  • ERROR
  • WARN
  • user="..."

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.

Take Quiz

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 checks
  • find() for the first match
  • rfind() for the last match
  • match_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 character
  • find() 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

  • String owns text
  • &str borrows text

Most string search methods are available on &str, and String can be borrowed as &str.

Comparisons

ConceptBest forReturnsNotes
chars()Per-character processingchar iteratorGood for parsing character rules
bytes()Raw byte processingu8 iteratorUseful for low-level work
contains()Check whether text existsboolSimplest existence check
find()First occurrenceOption<usize>Returns byte index
match_indices()All occurrencesIterator of (usize, &str)Great for repeated matches
split()Breaking text into partsIteratorUseful 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

TypeOwnershipTypical use
StringOwns the textStore and modify text
&strBorrows the textRead/search text without ownership
Take Quiz

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.

Take Quiz

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.
Take Quiz

Keep learning

Related questions

View more