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') }}"
Conditional Logic with regex_search
---
- 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
| Filter | Returns | Use When |
|---|---|---|
regex_search | First match (string or list) | You want one result |
regex_findall | All 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 Extract | Pattern | Example 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" |
([\w.+-]+@[\w-]+\.[\w.]+) | "user: a@b.com" → "a@b.com" | |
| Key=value | key\s*=\s*(.+) | "key = value" → "value" |
| Between quotes | "([^"]*)" | name="test" → "test" |
| Error code | error[:\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 }}
Related Articles
- Ansible regex_escape Filter
- Ansible Jinja2 Filters Guide
- Ansible selectattr & rejectattr
- Ansible lineinfile Module
- Ansible replace Module
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.