Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 42

Finding and Extracting Patterns with `string-match` and `match-string`

Recommended resources

Introduction

In our last lesson, we learned how to write the language of patterns—regular expressions. You now know how to construct a regex to describe specific sequences of text using anchors, character sets, and quantifiers. This is the foundation for all powerful text manipulation.

Today, we take the crucial next step. It's not enough to simply know if a pattern exists; we want to extract the valuable information that the pattern has found. This lesson addresses the learning outcome: Use string-match to find patterns and match-string to extract matched groups. We will learn how to find a match within a string and then pull out not only the entire matched text but also the specific subsections you've defined in your pattern.

This capability is central to your goal of building custom tools. Imagine writing a function that scans a document, finds all headings formatted like "Chapter 1: The Beginning," and extracts both the chapter number and the title into a list. Today's lesson provides the exact tools you need to do that.


1. The Concept of Capturing Groups

Before we dive into Elisp functions, let's solidify the core idea behind extracting sub-matches: capturing groups. In the previous lesson, you learned that $...$ groups parts of a regex. This grouping serves a second, powerful purpose: it tells the regex engine to "remember" the text that matched the expression inside the parentheses.

To help visualize this, let's watch a short segment from a video by The Coding Train. It explains the concept of capturing groups in a general programming context.

2.4: Regular Expressions: Capturing Groups - Programming with Text

This video, "Regular Expressions: Capturing Groups," clearly explains how parentheses in a regular expression create numbered groups that store parts of a match.

Watch the section from 01:50 to 03:19. Pay close attention to how the match is broken down into 'group 0' (the full match), 'group 1' (the first capture), and so on.

As the video explains, the regex engine automatically assigns numbers to the captured parts:

  • Group 0 is always the entire string that matched the whole regular expression.
  • Group 1 is the text captured by the first parenthetical group $...$.
  • Group 2 is the text captured by the second group, and so on.

For example, in the Elisp regex "Name: \$....\$", if it matches the text "Name: Anara", then:

  • Group 0 would be "Name: Anara".
  • Group 1 would be "Anara".

This ability to isolate parts of a match is what makes regular expressions so effective for data extraction.


2. Finding a Match: string-match

The first step in extracting data is to perform the search. In Elisp, the primary function for searching for a regex pattern within a string is string-match.

GNU Emacs Lisp Reference Manual - Searching and Matching

Let's consult the GNU Emacs Lisp Reference Manual for the official definition of string-match.

Read the short section describing the string-match function. You can find it by searching for "Function: string-match". Focus on what it takes as arguments and what it returns.

As the manual states, string-match has the following signature:
(string-match REGEXP STRING &optional START)

  • REGEXP: The regular expression to search for, as a string (remember to double your backslashes!).
  • STRING: The string to search within.
  • START (optional): An index in the string where the search should begin.

The function's return value is key:

  • If a match is found, it returns the starting index of the match.
  • If no match is found, it returns nil.

Let's try this in your *scratch* buffer.

;; The string we want to search
(setq log-entry "[2023-10-27] INFO: User 'anara' logged in.")

;; The pattern to find the log level (e.g., INFO, WARN, ERROR)
(setq pattern "]: \\([A-Z]+\\):")

;; Run the search
(string-match pattern log-entry)

If you evaluate this, the result will be 12. This tells you that a match was found starting at index 12 of the log-entry string.

Crucially, string-match doesn't just return a number. On success, it also populates a special, temporary storage area with information about the match—the match data. This data is what we'll use to extract the text.


3. Extracting the Match: match-string

After a successful call to string-match, you can access the captured text using the match-string function. This function is the practical application of the "capturing groups" concept from the video.

Simple Match Data (GNU Emacs Lisp Reference Manual)

The match-string function is the most direct way to get the text that was captured. Let's read its documentation.

Read the section for match-string. Note the count and in-string arguments, as they are both very important.

The function signature is:
(match-string COUNT &optional IN-STRING)

  • COUNT: A number specifying which group you want to extract (0 for the whole match, 1 for the first $...$ group, etc.).
  • IN-STRING: The original string you searched. This is important! When you use string-match, you must pass the same string here.

Let's continue our previous example. First, re-run the string-match to ensure the match data is set, then try extracting the groups.

;; Your scratch buffer should look like this.
;; Make sure you evaluate the string-match line again before the match-string lines.

(setq log-entry "[2023-10-27] INFO: User 'anara' logged in.")
(setq pattern "]: \\([A-Z]+\\):")

;; 1. Perform the search (this must be done first!)
(string-match pattern log-entry)
;; => 12

;; 2. Extract the full match (Group 0)
(match-string 0 log-entry)
;; => "]: INFO:"

;; 3. Extract the first captured group (Group 1)
(match-string 1 log-entry)
;; => "INFO"

Notice how match-string with 0 gives you everything that the regex "]: \$[A-Z]+\$:" matched, while match-string with 1 gives you only what was inside the $...$.


4. Putting It All Together: A Practical Example

Your background involves a lot of writing, editing, and content management. Let's imagine you have a system where file names contain metadata, like 2023-10-27_article_on-elisp.md. Let's write a small piece of code to parse this filename.

Our goal is to extract the date, the content type, and the slug.

  1. The Regex: We need a pattern with three capturing groups.

    "\\([0-9\\-]+\\)_\\([a-z]+\\)_\\(.*\\)\\.md"
    
    • \\([0-9\\-]+): Group 1. Matches one or more digits or hyphens (the date).
    • _: Matches the literal underscore separator.
    • \$[a-z]+\$: Group 2. Matches one or more lowercase letters (the type).
    • _: Another separator.
    • \$.*\$: Group 3. Matches any sequence of characters (the slug). The * is "greedy" and will match everything up to the...
    • \\.md: ...literal .md at the end.
  2. The Code: A common and robust pattern is to use an if statement. We only try to extract matches if string-match was successful.

(let ((filename "2023-10-27_article_on-elisp.md"))
  ;; Define the pattern with our three capture groups
  (let ((pattern "\\([0-9\\-]+\\)_\\([a-z]+\\)_\\(.*\\)\\.md"))
    
    ;; Check if the pattern matches the filename
    (if (string-match pattern filename)
        
        ;; If it matches, print the extracted parts
        (message "Successfully parsed!
Date: %s
Type: %s
Slug: %s"
                 (match-string 1 filename)  ; First group
                 (match-string 2 filename)  ; Second group
                 (match-string 3 filename)) ; Third group
      
      ;; If it doesn't match, print an error
      (message "Filename does not match expected format."))))

Copy this code into your *scratch* buffer and evaluate it with C-x C-e. You will see the parsed output appear in your minibuffer and the *Messages* buffer. This structure—if + string-match + match-string—is a fundamental recipe for text processing in Elisp.


5. Under the Hood: match-beginning and match-end

It's helpful to know that match-string is a convenience function built on more fundamental tools: match-beginning and match-end. These functions return the character indices of a match group, not the string itself.

  • (match-beginning COUNT): Returns the starting index of group COUNT.
  • (match-end COUNT): Returns the ending index of group COUNT.

You could get the same result as (match-string 1 filename) by doing:

(substring filename (match-beginning 1) (match-end 1))

You don't need to use these functions often for simple extraction, but knowing they exist is valuable. It clarifies that the "match data" is fundamentally a set of pointers (indices) into the original string, and helps demystify how Elisp handles matches internally.


Conclusion

Today you've bridged the gap between pattern description and data extraction. You now have the tools to not only find text but to pull out the specific pieces of information you care about.

Key Takeaways:

  • Capturing Groups $...$: These are the core mechanism for isolating parts of a pattern to be extracted. They are numbered starting from 1.
  • string-match: This function is your primary tool for searching a string with a regex. It returns a starting index on success (and sets the match data) or nil on failure.
  • match-string: Use this function after a successful string-match to retrieve the captured text. Group 0 refers to the entire match, while 1, 2, etc., refer to the corresponding $...$ groups.
  • The if pattern: Always wrap your match-string calls in a conditional that first checks if string-match was successful.

Preview of the Next Lesson

We can now find patterns and extract parts of them. What's next? Replacing them! In our next lesson, "Perform string substitution using 'replace-regexp-in-string'," we will learn how to use our captured groups to construct a new, transformed string. This will allow you to do powerful find-and-replace operations, like reformatting dates or swapping the order of names, bringing you even closer to automating your complex editorial and workflow tasks.