diff --git a/changelogs/fragments/11512-from_ini-delimiters.yaml b/changelogs/fragments/11512-from_ini-delimiters.yaml new file mode 100644 index 0000000000..728abf992e --- /dev/null +++ b/changelogs/fragments/11512-from_ini-delimiters.yaml @@ -0,0 +1,2 @@ +minor_changes: + - from_ini filter plugin - add ``delimiters`` parameter to allow correctly parsing more INI documents (https://github.com/ansible-collections/community.general/issues/11506, https://github.com/ansible-collections/community.general/pull/11512). \ No newline at end of file diff --git a/plugins/filter/from_ini.py b/plugins/filter/from_ini.py index 1d3a2f1cec..bd40e3ff97 100644 --- a/plugins/filter/from_ini.py +++ b/plugins/filter/from_ini.py @@ -16,6 +16,14 @@ options: description: A string containing an INI document. type: string required: true + delimiters: + description: A list of characters used as delimiters in the INI document. + type: list + elements: string + default: + - "=" + - ":" + version_added: 12.4.0 seealso: - plugin: community.general.to_ini plugin_type: filter @@ -53,13 +61,17 @@ from configparser import ConfigParser from io import StringIO from ansible.errors import AnsibleFilterError +from ansible.module_utils.common.collections import is_sequence class IniParser(ConfigParser): """Implements a configparser which is able to return a dict""" - def __init__(self): - super().__init__(interpolation=None) + def __init__(self, delimiters=None): + if delimiters is None: + super().__init__(interpolation=None) + else: + super().__init__(interpolation=None, delimiters=delimiters) self.optionxform = str def as_dict(self): @@ -74,13 +86,21 @@ class IniParser(ConfigParser): return d -def from_ini(obj): +def from_ini(obj, delimiters=None): """Read the given string as INI file and return a dict""" if not isinstance(obj, str): raise AnsibleFilterError(f"from_ini requires a str, got {type(obj)}") + if delimiters is not None: + if not is_sequence(delimiters): + raise AnsibleFilterError(f"from_ini's delimiters parameter must be a sequence, got {type(delimiters)}") + delimiters = tuple(delimiters) + if not all(isinstance(elt, str) for elt in delimiters): + raise AnsibleFilterError( + f"from_ini's delimiters parameter must be a sequence of strings, got {delimiters!r}" + ) - parser = IniParser() + parser = IniParser(delimiters=delimiters) try: parser.read_file(StringIO(obj)) diff --git a/tests/integration/targets/filter_from_ini/tasks/main.yml b/tests/integration/targets/filter_from_ini/tasks/main.yml index abb92dfc55..8f037eecd3 100644 --- a/tests/integration/targets/filter_from_ini/tasks/main.yml +++ b/tests/integration/targets/filter_from_ini/tasks/main.yml @@ -41,6 +41,32 @@ - 'ini_file_content.content | b64decode | community.general.from_ini == ini_test_dict' +- name: 'Define another ini_test_dict' + ansible.builtin.set_fact: + ini_test_dict: + section_name: + 'key_name * : with spaces': 'key value' + +- name: 'Write another INI file that reflects ini_test_dict to {{ ini_test_file }}' + ansible.builtin.copy: + dest: '{{ ini_test_file }}' + content: | + [section_name] + key_name * : with spaces = key value + +- name: 'Slurp the other test file: {{ ini_test_file }}' + ansible.builtin.slurp: + src: '{{ ini_test_file }}' + register: 'ini_file_content' + +- name: >- + Ensure defined ini_test_dict is the same when retrieved + from other {{ ini_test_file }} + ansible.builtin.assert: + that: + - 'ini_file_content.content | b64decode | community.general.from_ini(delimiters=["="]) == + ini_test_dict' + - name: 'Create a file that is not INI formatted: {{ ini_bad_file }}' ansible.builtin.copy: dest: '{{ ini_bad_file }}'