Concepts / Matching Words in a String with Regular Expressions

Matching Words in a String with Regular Expressions

A reg ex_iterator reports the matches of a pattern over a range, and it cannot be pointed directly at an istream, so text should be read into a string first.

  • Programming

A Program That Silently Drops a Word

Imagine a small program whose job is to print every whitespace-separated word in a string. It builds a regular expression, creates a regex_iterator over the string's start and end positions, and prints the first capture group of each match. The tool is neat and compact, and that neatness invites a dangerous assumption: that the iterator will report every piece of text you care about. It will not. Its results depend on exactly what the pattern allows, and the first word of a string is often silently left out.

What do you think happens?

Given the text "to be or" and the pattern R"(\s+(\w+))", which words does the program print, and in what order?

  • to, be, or

  • be, or

  • to, be

  • only or

Reveal answer
Answer:

be, or

The word to sits at the very start of the text. The pattern requires a whitespace character before the word, and nothing precedes the first word, so no match can begin there. The two matches are the space before be and the space before or, and only the captured words are printed. Nothing warns you that to was skipped.

Tracing Which Characters Get Matched

The question to ask at every step is whether the pattern could possibly begin a match at that position. Take the text "to be or" and lay out its characters by index. The pattern R"(\s+(\w+))" needs whitespace first, then a run of word characters. The whitespace at index 2 is followed by be, so characters 2 through 4 form the first match. The whitespace at index 5 is followed by or, so characters 5 through 7 form the second match. The letters t and o at indexes 0 and 1 never take part in any match, because no whitespace comes before them.

tindex 0, skippedoindex 1, skippedspaceindex 2, match 1bindex 3, match 1 captureeindex 4, match 1 capturespaceindex 5, match 2oindex 6, match 2 capturerindex 7, match 2 capture
For the text to be or, with the pattern requiring whitespace followed by word characters, which character cells belong to a match and which are skipped?

The changed cells are the only ones the iterator reports as part of a match. Only the captured letters, be and or, are printed, because the program reads capture group 1. The whitespace cells are consumed as part of each match but never shown to the caller. Trace the program against the index map and the missing first word becomes obvious rather than mysterious.

How the Pattern Parts Work Together

The pattern R"(\s+(\w+))" has two parts that cooperate. The outer part, \s+, demands at least one whitespace character. The inner group, (\w+), captures one or more word characters, and that captured text is what the program prints through the index [1]. A match therefore consists of whitespace followed immediately by a word, and only the word is exposed to the caller.

Because the whitespace is part of what must match, the leading space is consumed as part of the match rather than being printed. This is why a word at the start of the string cannot be reported: there is no whitespace character available to satisfy the first half of the pattern.

cpp
Output
be
or
Notes on the code
  • The regex encodes the whitespace requirement that causes the first word to be skipped.
  • The iterator is built over the string's start and end positions, not over a stream.
  • The default sregex_iterator named end is the end-of-sequence marker.
  • (*it)[1] reads capture group 1, which is the word without its leading space.

The example string is illustrative and was chosen to show the mechanism; the printed result for this exact input follows from the pattern's rules as described above.

Removing the Whitespace Requirement

If you remove the whitespace requirement and use the simpler pattern R"((\w+))", the iterator no longer needs anything in front of a word. The source shows that this pattern does report aa, the word the first version missed. Its output for the source's test string is: aa as asd e asdf asdfg.

The simplified pattern also reports a stray letter e and the word asdf as separate matches. The input contains a caret character between them, and a caret is not a word character, so the run of word characters breaks at that point. A pattern does not know what you meant; it knows only which character classes it accepts. Each change to the pattern changes which substrings are eligible, so you must check the results against the input you actually have.

Worked example

Predicting matches for the simplified pattern

Using the text to be or and the pattern R"((\w+))", list every match the iterator reports.

  1. 1
    Check the start

    The pattern needs only a word, so a match can begin at index 0 where t is a word character.

  2. 2
    Take the first word

    The run of word characters from index 0 is to, which becomes the first match.

  3. 3
    Skip the whitespace

    The space at index 2 is not a word character, so no match includes it. The search moves on to the next word.

  4. 4
    Take the remaining words

    be and or each form their own match in the same way.

Result

The iterator reports to, be, and or. Compared with the whitespace-based pattern, the only change is that to is now included.

Why an Iterator Cannot Read From a Stream

A regex_iterator reports the matches of a pattern over a range, and the source states plainly that you cannot iterate directly over an istream with one. The iterator needs a stable range with a start and an end, so the text must first reach a string or another range the iterator can accept. Only then can the iterator look for matches across the whole text.

input streamtext arrives herestringstable range with start andendregex_iteratorwalks the rangematcheseach place the patternmatchesprinted wordscapture group 1read intobegin and endreportsgroup 1
What is the data path from an input stream to the printed matches, and why does the text have to pass through a string?

How the Iterator Advances and Stops

The regex_iterator is described as bidirectional: it can move forward through matches and also move backward. Its behavior is governed by the sequence of matches the pattern produces over the range you gave it. A loop advances the iterator, reads the current match, and keeps going until the iterator equals the default-constructed value, written sregex_iterator{}. The source identifies this default value as the only possible end-of-sequence, so comparing against it is the correct way to know matching has finished.

start of textfirst searchmatch: beprintedmatch: orprintedsregex_iterator{}end-of-sequence, loop exitsfirst matchadvanceno more matches
After each match is found, where does the iterator go next, and what happens when no matches remain?

Read the loop in the earlier code this way: each pass handles one match, the advance step moves to the next one, and the comparison against end is the single condition that stops the loop. Because the default iterator is the only end marker the source names, the loop should be written to compare against it rather than against some other guessed value.

What Reading and Writing Allow

Dereferencing a regex_iterator gives you access to the match it currently points to, and capture groups let you pick out the part you need. This is reading, and it is what the program does when it prints (*it)[1]. The iterator, however, is not a way to write values into the scanned text. The source states that you cannot write through a regex_iterator. It lets you inspect what matched; it does not give you a route for changing the text it scanned.

ActionAllowed?Why
Dereference the iterator to inspect the current matchYesReading the match is the iterator's purpose
Read capture group 1 to get just the wordYesCapture groups expose the part of the match you chose
Write new text through the iterator into the scanned rangeNoThe iterator is not a way to write values into the scanned text
Point the iterator directly at an istreamNoThe range must be stable, so text is first read into a string

What reading through a regex_iterator allows versus what writing through it forbids

Mistakes Beginners Make

  • Common mistake

    Expecting the first word to be reported by a whitespace-based pattern

    What it looks like

    Using R"(\s+(\w+))" on to be or and expecting to see to in the output

    Why it fails

    The pattern needs whitespace before each word, and the first word has nothing in front of it, so no match can start there.

    Do this instead

    Use R"((\w+))" when the first word must be included, and check the results against the actual input.

  • Common mistake

    Pointing the iterator directly at an istream

    What it looks like

    Constructing a regex_iterator over the input stream and looping over it

    Why it fails

    The iterator needs a stable range with a start and an end, and an istream is consumed as it is read.

    Do this instead

    Read the text into a std::string first, then construct the iterator over that string's start and end positions.

  • Common mistake

    Trying to change the scanned text through the iterator

    What it looks like

    Assigning a new value through the dereferenced iterator to replace a matched word

    Why it fails

    The source states that a regex_iterator cannot be used to write into the scanned text.

    Do this instead

    Read matches through the iterator, then build any new text in a separate string.

  • Common mistake

    Comparing the loop against a guessed end value

    What it looks like

    Stopping when the iterator reaches some arbitrary position

    Why it fails

    The default sregex_iterator is the only end-of-sequence the source identifies.

    Do this instead

    Loop while the iterator is not equal to sregex_iterator{}.

Practice: Predict the Matches

MEDIUM

The text is "aa as". Using the whitespace-based pattern R"(\s+(\w+))", predict which words the program prints. Then predict the output if the pattern is changed to R"((\w+))". Explain each result by naming the characters each match covers.

Hints
  • Ask whether a match can begin at index 0, where aa starts.
  • The whitespace-based pattern needs a space character before each word it reports.
  • The simplified pattern needs only a run of word characters, so it can start at index 0.

Key Takeaways

  • A regex_iterator reports the matches of a pattern over a range, and it cannot be pointed directly at an istream, so text must first be read into a string.
  • A whitespace-based pattern such as R"(\s+(\w+))" cannot report a word at the very start of the text, because nothing precedes that word; R"((\w+))" finds it.
  • Reading through a regex_iterator, including dereferencing it to get capture groups, is allowed; writing through it into the scanned text is not.
  • The default sregex_iterator{} is the only end-of-sequence, so loops should advance the iterator and stop when it equals that default value.

Related concepts

More concepts starting with M