Introduction

The regex_search filter searches a string for a regular expression pattern and returns the match. If the pattern includes capture groups, it returns the captured text. If there's no match, it returns an empty string. It's the most versatile text extraction tool in Ansible — use it to pull version numbers, IP addresses, error codes, or any structured text out of command output.

Basic Syntax

# Return the full match
{{ my_string | regex_search('pattern') }}

# Return capture group(s)
{{ my_string | regex_search('pattern', '\\1') }}

# Case-insensitive
{{ my_string | regex_search('pattern', ignorecase=True) }}

# Multiline mode (^ and $ match line boundaries)
{{ my_string | regex_search('pattern', multiline=True) }}

Simple Match Examples

---
- name: regex_search basic examples
  hosts: localhost
  gather_facts: false
  vars:
    version_string: "ansible-core 2.16.3"
    log_line: "2026-04-12 ERROR [auth] Login failed for user admin from 10.0.0.5"
    config_line: "max_connections = 256"

  tasks:
    - name: Extract version number
      ansible.builtin.debug:
        msg: "{{ version_string | regex_search('[0-9]+\\.[0-9]+\\.[0-9]+') }}"
      # Output: "2.16.3"

    - name: Extract IP address
      ansible.builtin.debug:
        msg: "{{ log_line | regex_search('[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+') }}"
      # Output: "10.0.0.5"

    - name: Extract number after equals
      ansible.builtin.debug:
        msg: "{{ config_line | regex_search('=\\s*(\\d+)', '\\1') }}"
      # Output: "256"

    - name: Check if pattern exists (boolean)
      ansible.builtin.debug:
        msg: "Contains ERROR"
      when: log_line | regex_search('ERROR')

Capture Groups

Capture groups () extract specific parts of the match:

---
- name: Capture group examples
  hosts: localhost
  gather_facts: false
  vars:
    url: "https://api.example.com:8443/v2/users"
    version_output: "Python 3.12.1 (main, Jan 15 2026)"
    kernel: "Linux 6.5.0-44-generic #44~22.04.1-Ubuntu SMP"

  tasks:
    - name: Extract hostname and port from URL
      ansible.builtin.debug:
        msg:
          host: "{{ url | regex_search('https?://([^:/]+)', '\\1') }}"
          port: "{{ url | regex_search(':([0-9]+)/', '\\1') }}"
      # host: "api.example.com", port: "8443"

    - name: Extract Python major.minor version
      ansible.builtin.debug:
        msg: "{{ version_output | regex_search('Python (\\d+\\.\\d+)', '\\1') }}"
      # Output: "3.12"

    - name: Extract kernel version
      ansible.builtin.debug:
        msg: "{{ kernel | regex_search('(\\d+\\.\\d+\\.\\d+)', '\\1') }}"
      # Output: "6.5.0"

    - name: Multiple capture groups
      ansible.builtin.set_fact:
        match: "{{ 'John Smith, age 42' | regex_search('(\\w+) (\\w+), age (\\d+)', '\\1', '\\2', '\\3') }}"
      # match: ["John", "Smith", "42"]

Multiline Matching

---
- name: Extract from multiline content
  hosts: all
  tasks:
    - name: Read config file
      ansible.builtin.slurp:
        src: /etc/myapp/config.ini
      register: config_raw

    - name: Decode content
      ansible.builtin.set_fact:
        config: "{{ config_raw.content | b64decode }}"

    - name: Extract database host from config
      ansible.builtin.set_fact:
        db_host: "{{ config | regex_search('^db_host\\s*=\\s*(.+)$', '\\1', multiline=True) }}"
      # Matches "db_host = 10.0.0.5" on its own line

    - name: Extract all key=value pairs from a section
      ansible.builtin.debug:
        msg: "DB host is {{ db_host }}"

Command Output Parsing

---
- name: Parse command output with regex_search
  hosts: all
  tasks:
    - name: Get disk usage
      ansible.builtin.command: df -h /
      register: df_output

    - name: Extract usage percentage
      ansible.builtin.set_fact:
        disk_pct: "{{ df_output.stdout | regex_search('(\\d+)%', '\\1') }}"

    - name: Alert if disk > 80%
      ansible.builtin.debug:
        msg: "⚠️ Disk at {{ disk_pct }}%!"
      when: disk_pct | int > 80

    - name: Get memory info
      ansible.builtin.command: free -m
      register: mem_output

    - name: Extract total memory
      ansible.builtin.set_fact:
        total_mem: "{{ mem_output.stdout | regex_search('Mem:\\s+(\\d+)', '\\1') }}"

    - name: Get Ansible version
      ansible.builtin.command: ansible --version
      register: ansible_ver

    - name: Extract core version
      ansible.builtin.debug:
        msg: "{{ ansible_ver.stdout | regex_search('ansible.*core (\\d+\\.\\d+\\.\\d+)', '\\1') }}"
---
- name: Route tasks based on content
  hosts: all
  tasks:
    - name: Get OS release
      ansible.builtin.slurp:
        src: /etc/os-release
      register: os_release_raw

    - name: Set OS facts
      ansible.builtin.set_fact:
        os_content: "{{ os_release_raw.content | b64decode }}"

    - name: Detect Ubuntu
      ansible.builtin.set_fact:
        is_ubuntu: "{{ os_content | regex_search('ID=ubuntu') | length > 0 }}"

    - name: Detect RHEL family
      ansible.builtin.set_fact:
        is_rhel: "{{ os_content | regex_search('ID_LIKE=.*rhel') | length > 0 }}"

    - name: Get OS version
      ansible.builtin.set_fact:
        os_version: "{{ os_content | regex_search('VERSION_ID=\"?(\\d+)', '\\1') }}"

regex_search vs regex_findall

FilterReturnsUse When
regex_searchFirst match (string or list)You want one result
regex_findallAll matches (list)You want every occurrence
- name: Compare search vs findall
  vars:
    text: "IPs: 10.0.0.1, 10.0.0.2, 10.0.0.3"
  ansible.builtin.debug:
    msg:
      first_ip: "{{ text | regex_search('\\d+\\.\\d+\\.\\d+\\.\\d+') }}"
      # "10.0.0.1"
      all_ips: "{{ text | regex_findall('\\d+\\.\\d+\\.\\d+\\.\\d+') }}"
      # ["10.0.0.1", "10.0.0.2", "10.0.0.3"]

Common Patterns

What to ExtractPatternExample Input → Output
Version X.Y.Z(\d+\.\d+\.\d+)"v2.16.3-rc1" → "2.16.3"
IP address(\d+\.\d+\.\d+\.\d+)"host 10.0.0.1 port" → "10.0.0.1"
Email([\w.+-]+@[\w-]+\.[\w.]+)"user: a@b.com" → "a@b.com"
Key=valuekey\s*=\s*(.+)"key = value" → "value"
Between quotes"([^"]*)"name="test" → "test"
Error codeerror[:\s]+(\d+)"error: 403" → "403"
Port number:(\d{2,5})"host:8080/path" → "8080"
UUID[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}full UUID

Troubleshooting

Empty String Result

regex_search returns "" (empty string) when no match is found. Test for this:

- name: Handle no match
  ansible.builtin.set_fact:
    result: "{{ my_var | regex_search('pattern') }}"

- name: Check if found
  ansible.builtin.debug:
    msg: "Not found!"
  when: result == "" or result is none

Backslash Escaping

In YAML, you need double backslashes for regex special characters:

# Wrong — single backslash gets consumed by YAML
msg: "{{ text | regex_search('\d+') }}"

# Right — double backslash
msg: "{{ text | regex_search('\\d+') }}"

Capture Group Returns a List

When you use capture group arguments (\\1, \\2), the result is a list, not a string:

# This returns ["3.12"] not "3.12"
{{ text | regex_search('Python (\\d+\\.\\d+)', '\\1') }}

# To get a string, use first or join
{{ text | regex_search('Python (\\d+\\.\\d+)', '\\1') | first }}

Conclusion

regex_search is Ansible's Swiss Army knife for text extraction. Use it without capture groups for simple matching, with \\1 for extracting specific parts, and with multiline=True for config file parsing. Always double-escape backslashes in YAML (\\d not \d). When you need all matches instead of just the first, switch to regex_findall. The most common pattern: extract a version/IP/value from command output with register → regex_search.