How to generate a list of formatted strings

I would like to define a variable called test_users with a list of test usernames:

test_users:
  - testuser_001
  - testuser_002
  ...
  - testuser_100

I would prefer not to type them in by hand, or generate the list and paste it into the YAML file. I imagined that I could do something like

test_users: "{{ range(1, 100, 1) | format('testuser_%3d') | list }}"

but this doesn’t work, as the format filter takes the format string as input rather than the item(s) to be formatted.

What would be a good way to achieve this?

Thanks!

https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_filters.html#searching-strings-with-regular-expressions

Below is a section with

To replace text in a string with regex, use the ansible.builtin.regex_replace filter:

---
- hosts: localhost
  tasks:
    - debug:
        var: range(1, 10) | map('regex_replace', '^', 'testuser_') | list 

results in

ok: [localhost] => {
    "range(1, 10) | map('regex_replace', '^', 'testuser_') | list": [
        "testuser_1",
        "testuser_2",
        "testuser_3",
        "testuser_4",
        "testuser_5",
        "testuser_6",
        "testuser_7",
        "testuser_8",
        "testuser_9"
    ]
}
1 Like

Thanks - that’s a good option. Do you know if there is a way to get the zero-padding? That’s less important, but I thought it should be doable with Jinja. It’s a shame the format function can’t be used for this.

I found another option:

      test_users: "{{ (['testuser_%03d'] * 10) | join(',') | format(*range(1,11)) | split(',') }}"

inspired by this answer. It lacks elegance because it requires you to keep the number of format strings and the length of the range the same, and it goes via strings, so you have to be careful that your separator doesn’t appear in the string, but it does the job in a single line without having to make a custom filter. Is there a way to improve on this?

this remembers me back to old times with gnu octave.

Another possibility when you process the variable item after item…

    - debug:
        var: USER 
      vars:
        USER: "{{ 'testuser_%03d' | format(item) }}"
      loop: "{{ range(1,100) }}"

Instead of debug, you can use include_tasks: some_taskfile.yml and work with the USER var.

Yeah, the task loop is what I started with, but I actually do need this as a list variable, because it will be joined to another list.

Thanks!

What you need to be able to use is the map filter, but unfortunately the built in jinja2 format filter wasn’t really written with usability from the map filter in mind, and thus the arguments it takes are reversed from what you need.

Instead of making some overly complicated jinja template in your playbook, I’d recommend making a new custom filter for your needs. This could be as simple as making your own version of format that works the way you need such as:

# saved as filter_plugins/ianhinder.py
def ianhinder_format(value, fmt):
    return fmt % value


class FilterModule:
    def filters(self):
        return {
            'ianhinder_format': ianhinder_format,
        }

And then a task to use it:

    - debug:
        msg: "{{ range(1, 101, 1) | map('ianhinder_format', 'testuser_%03d') }}"

Note: Needed to use %03d instead of %3d since the latter does space padding and not zero padding. And to include 100 in the range, you have to set the stop to 101 as it’s not inclusive of the stop.

Produces:

ok: [localhost] => {
    "msg": [
        "testuser_001",
        "testuser_002",
        "testuser_003",
        "testuser_004",
        "testuser_005",
        "testuser_006",
        "testuser_007",
        "testuser_008",
[SNIP]
        "testuser_100"
    ]
}

Thanks. That certainly is the most ergonomic in the playbook, though it has the annoyance of having to implement it yourself. Yes, most of the examples I found for similar tasks when searching said you need to write your own filter in Python. It seems that Jinja2 is not quite powerful enough to express all that is generally needed, and it needs to be augmented with Python code even for quite simple use cases.

I’d be tempted to use the ansible.builtin.regex_replace filter for this… :person_shrugging: