Ansible regex_search & regex_replace Filters: Pattern Matching Guide
By Luca Berton · Published 2024-01-01 · Category: troubleshooting
Complete guide to Ansible regex_search and regex_replace filters. Extract text with regex patterns, find and replace strings, validate formats, parse log.
Ansible's regex_search and regex_replace filters let you extract and transform text using regular expressions — parse version numbers, validate formats, clean strings, and extract values from command output.
regex_search: Find and Extract
Returns the first match (or None if no match):
- name: Extract version number
ansible.builtin.set_fact:
version: "{{ 'nginx/1.24.0' | regex_search('[0-9]+\\.[0-9]+\\.[0-9]+') }}"
# Result: "1.24.0"
- name: Check if string matches pattern
ansible.builtin.debug:
msg: "Valid IP address"
when: my_var | regex_search('^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$')
See also: Ansible regex_replace Filter: Find & Replace with Regex (Complete Guide)
Capture Groups
# Extract specific parts using capture groups
- name: Extract major version
ansible.builtin.set_fact:
major: "{{ 'Python 3.11.5' | regex_search('([0-9]+)\\.([0-9]+)\\.([0-9]+)', '\\1') }}"
minor: "{{ 'Python 3.11.5' | regex_search('([0-9]+)\\.([0-9]+)\\.([0-9]+)', '\\2') }}"
patch: "{{ 'Python 3.11.5' | regex_search('([0-9]+)\\.([0-9]+)\\.([0-9]+)', '\\3') }}"
# major: ["3"], minor: ["11"], patch: ["5"]
# First element of the list
- ansible.builtin.debug:
msg: "Major version: {{ major | first }}"
regex_replace: Find and Replace
# Basic replacement
- name: Remove comments from config
ansible.builtin.set_fact:
clean_config: "{{ raw_config | regex_replace('#.*$', '', multiline=True) }}"
# Replace with capture groups
- name: Swap first and last name
ansible.builtin.set_fact:
formatted: "{{ 'John Smith' | regex_replace('^(\\w+)\\s(\\w+)$', '\\2, \\1') }}"
# Result: "Smith, John"
# Remove non-alphanumeric characters
- name: Sanitize filename
ansible.builtin.set_fact:
safe_name: "{{ user_input | regex_replace('[^a-zA-Z0-9_-]', '_') }}"
See also: Ansible combine Filter: Merge Dictionaries & Override Defaults (Guide)
Common Patterns
Parse Command Output
- name: Get kernel version
ansible.builtin.command: uname -r
register: kernel_output
changed_when: false
- name: Extract major.minor
ansible.builtin.set_fact:
kernel_version: "{{ kernel_output.stdout | regex_search('^([0-9]+\\.[0-9]+)') }}"
# "6.8.0-41-generic" → "6.8"
Validate Email Format
- name: Validate email
ansible.builtin.assert:
that:
- user_email | regex_search('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$')
fail_msg: "Invalid email: {{ user_email }}"
Extract IP from Text
- name: Extract IP address
ansible.builtin.set_fact:
ip_address: "{{ log_line | regex_search('([0-9]{1,3}\\.){3}[0-9]{1,3}') }}"
Clean Whitespace
# Remove extra whitespace
- ansible.builtin.set_fact:
clean: "{{ messy_string | regex_replace('\\s+', ' ') | trim }}"
Parse Key=Value Pairs
- name: Extract value from config line
ansible.builtin.set_fact:
db_port: "{{ config_line | regex_search('port=([0-9]+)', '\\1') | first }}"
# "host=localhost port=5432 dbname=myapp" → "5432"
Strip HTML Tags
- ansible.builtin.set_fact:
plain_text: "{{ html_content | regex_replace('<[^>]+>', '') }}"
Case-Insensitive Matching
- ansible.builtin.set_fact:
found: "{{ text | regex_search('error', ignorecase=True) }}"
See also: Ansible dict2items Filter: Convert Dictionaries to Lists (Guide)
Multiline Matching
- ansible.builtin.set_fact:
block: "{{ multiline_text | regex_search('BEGIN(.+?)END', '\\1', multiline=True, dotall=True) }}"
Using regex in when Conditions
- name: Only run on RHEL 8.x or 9.x
ansible.builtin.debug:
msg: "Running on RHEL {{ ansible_distribution_version }}"
when: ansible_distribution_version | regex_search('^[89]\\.')
- name: Skip if hostname contains 'test'
ansible.builtin.include_tasks: production-tasks.yml
when: not (inventory_hostname | regex_search('test|staging|dev'))
regex_findall: All Matches
# Find ALL occurrences (not just first)
- name: Extract all IPs from log
ansible.builtin.set_fact:
all_ips: "{{ log_content | regex_findall('([0-9]{1,3}\\.){3}[0-9]{1,3}') }}"
# Find all email addresses
- ansible.builtin.set_fact:
emails: "{{ text | regex_findall('[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}') }}"
regex_search vs match vs search
# regex_search — Jinja2 filter, returns match or None
{{ string | regex_search('pattern') }}
# match — Jinja2 test, matches from START of string
{{ string is match('pattern') }}
# search — Jinja2 test, matches ANYWHERE in string
{{ string is search('pattern') }}
# Examples
- ansible.builtin.debug:
msg: "Starts with ansible"
when: my_string is match('ansible.*')
- ansible.builtin.debug:
msg: "Contains error anywhere"
when: my_string is search('error')
- ansible.builtin.set_fact:
version: "{{ my_string | regex_search('[0-9]+\\.[0-9]+') }}"
FAQ
Why does regex_search return None instead of matching?
Common causes: not escaping backslashes (use \\d not \d in YAML), or the pattern expects start/end anchors. Test your regex at regex101.com first, then double-escape for YAML.
How do I use capture groups with regex_search?
Pass the group reference as additional arguments: regex_search('pattern(group)', '\\1'). This returns a list, so use | first to get the string value.
What regex flavor does Ansible use?
Python re module — supports \d, \w, \s, lookahead (?=...), lookbehind (?<=...), and named groups (?P.
How do I make regex_replace work on multiline strings?
Add multiline=True for ^ and $ to match line boundaries, or dotall=True for . to match newlines.
Conclusion
Use regex_search to extract data from strings, regex_replace to transform strings, and regex_findall to find all occurrences. Always double-escape backslashes in YAML (\\d not \d). For simple contains/startswith checks, prefer is search and is match tests — they're more readable than regex_search for boolean conditions.
Related Articles
• Ansible map vs selectattr vs json_query • Ansible Jinja2 Templates Guide • Ansible lineinfile Module Cookbook • Ansible Conditionals GuideCategory: troubleshooting