Introduction
Ansible provides three filters for string manipulation: replace for simple substitution, regex_replace for pattern-based substitution, and regex_search for extracting matches. Choosing the right one avoids overcomplicated expressions for simple tasks and underpowered tools for complex ones.
Quick Comparison
| Filter | Purpose | Input | Output |
|---|---|---|---|
replace | Simple string substitution | Exact string match | Modified string |
regex_replace | Pattern-based substitution | Regex pattern | Modified string |
regex_search | Extract matching text | Regex pattern | Matched string or list |
replace — Simple Substitution
- vars:
hostname: "web-server-01.prod.example.com"
path: "/var/log/app/debug.log"
version: "v2.5.0-beta"
tasks:
# Simple string replacement
- debug:
msg: "{{ hostname | replace('.prod.', '.staging.') }}"
# → web-server-01.staging.example.com
# Remove a substring
- debug:
msg: "{{ version | replace('-beta', '') }}"
# → v2.5.0
# Replace path separators
- debug:
msg: "{{ path | replace('/', '\\') }}"
# → \var\log\app\debug.log
# Chain replacements
- debug:
msg: "{{ hostname | replace('-', '_') | replace('.', '-') }}"
# → web_server_01-prod-example-com
regex_replace — Pattern Substitution
- vars:
log_line: "2026-04-14 10:30:45 ERROR [main] Connection timeout after 30s"
filename: "report_2026-04-14_final_v3.pdf"
ip_list: "192.168.1.10, 10.0.0.5, 172.16.0.1"
tasks:
# Replace pattern with capture groups
- debug:
msg: "{{ log_line | regex_replace('^(\\d{4}-\\d{2}-\\d{2}).*', '\\1') }}"
# → 2026-04-14
# Remove all digits
- debug:
msg: "{{ filename | regex_replace('[0-9]', '') }}"
# → report__--__final_v.pdf
# Replace multiple spaces with single
- debug:
msg: "{{ ' too many spaces ' | regex_replace('\\s+', ' ') | trim }}"
# → too many spaces
# Mask IP addresses
- debug:
msg: "{{ ip_list | regex_replace('(\\d+\\.\\d+\\.\\d+\\.)\\d+', '\\1***') }}"
# → 192.168.1.***, 10.0.0.***, 172.16.0.***
# Convert camelCase to snake_case
- debug:
msg: "{{ 'myVariableName' | regex_replace('([A-Z])', '_\\1') | lower }}"
# → my_variable_name
# Add prefix to lines
- debug:
msg: "{{ multiline_text | regex_replace('^', '# ', multiline=True) }}"
regex_search — Extract Matches
- vars:
log_line: "ERROR [2026-04-14 10:30:45] Request failed: status=503, latency=2450ms"
version_string: "ansible-core 2.20.1 (python 3.12.4)"
url: "https://api.example.com:8443/v2/users?page=3"
tasks:
# Extract first match
- debug:
msg: "{{ log_line | regex_search('status=(\\d+)', '\\1') }}"
# → ['503']
# Extract version number
- debug:
msg: "{{ version_string | regex_search('(\\d+\\.\\d+\\.\\d+)') }}"
# → 2.20.1
# Extract port from URL
- debug:
msg: "{{ url | regex_search(':(\\d+)/', '\\1') | first }}"
# → 8443
# Extract multiple groups
- debug:
msg: "{{ log_line | regex_search('status=(\\d+), latency=(\\d+)ms', '\\1', '\\2') }}"
# → ['503', '2450']
# Check if pattern exists (boolean)
- debug:
msg: "Has error: {{ log_line | regex_search('ERROR') is not none }}"
# → Has error: True
# Use in conditionals
- name: Alert on high latency
ansible.builtin.debug:
msg: "High latency detected!"
when: (log_line | regex_search('latency=(\\d+)', '\\1') | first | int) > 1000
regex_findall — All Matches
- vars:
text: "IPs: 192.168.1.10, 10.0.0.5, 172.16.0.1, 8.8.8.8"
html: '<a href="page1.html">Link 1</a> <a href="page2.html">Link 2</a>'
tasks:
# Find all IP addresses
- debug:
msg: "{{ text | regex_findall('\\d+\\.\\d+\\.\\d+\\.\\d+') }}"
# → ['192.168.1.10', '10.0.0.5', '172.16.0.1', '8.8.8.8']
# Find all href values
- debug:
msg: "{{ html | regex_findall('href=\"([^\"]+)\"') }}"
# → ['page1.html', 'page2.html']
# Count occurrences
- debug:
msg: "Found {{ text | regex_findall('\\d+\\.\\d+\\.\\d+\\.\\d+') | length }} IPs"
Practical Examples
Parse Structured Output
- name: Get nginx version
ansible.builtin.command: nginx -v
register: nginx_ver
changed_when: false
- name: Extract version
ansible.builtin.set_fact:
nginx_version: "{{ nginx_ver.stderr | regex_search('nginx/(\\d+\\.\\d+\\.\\d+)', '\\1') | first }}"
# nginx_ver.stderr: "nginx version: nginx/1.24.0"
# → "1.24.0"
Sanitize Input
- name: Clean hostname for use as filename
ansible.builtin.set_fact:
safe_name: "{{ inventory_hostname | regex_replace('[^a-zA-Z0-9_-]', '_') }}"
# web-1.prod.example.com → web-1_prod_example_com
Transform Config Values
- name: Convert memory string to MB
ansible.builtin.set_fact:
memory_mb: "{{ memory_string | regex_search('(\\d+)') | int }}"
# "4096MB" → 4096, "8G" → 8
Common Mistakes
# ❌ Using regex_replace for simple substitution
msg: "{{ text | regex_replace('old', 'new') }}"
# ✅ Use replace — simpler and faster
msg: "{{ text | replace('old', 'new') }}"
# ❌ Forgetting to escape backslashes in YAML
msg: "{{ text | regex_replace('\d+', 'NUM') }}"
# YAML interprets \d as literal d!
# ✅ Double-escape or use single quotes
msg: "{{ text | regex_replace('\\d+', 'NUM') }}"
# ❌ regex_search returns None on no match
- set_fact:
port: "{{ url | regex_search(':(\\d+)') }}"
# If no port in URL → port is "None" (string!)
# ✅ Handle no-match case
- set_fact:
port: "{{ url | regex_search(':(\\d+)', '\\1') | default(['443']) | first }}"
Related Articles
- Ansible Jinja2 Templates
- Ansible Filter Plugins
- Ansible map vs selectattr vs json_query
- Ansible lineinfile Module
Conclusion
replace for exact string substitution (fast, simple). regex_replace for pattern-based substitution with capture groups. regex_search for extracting text that matches a pattern. regex_findall for finding all occurrences. Rule of thumb: if your search string is literal text, use replace. If you need patterns, groups, or wildcards, use the regex variants. Always double-escape backslashes in YAML strings.