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

FilterPurposeInputOutput
replaceSimple string substitutionExact string matchModified string
regex_replacePattern-based substitutionRegex patternModified string
regex_searchExtract matching textRegex patternMatched 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 }}"

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.