# Working with Ansible Lineinfile: A Tutorial for Beginners

When managing multiple systems, the ability to make precise, controlled
changes to configuration files becomes essential. This is where [Ansible](https://betterstack.com/community/guides/linux/ansible-getting-started/)'s
`lineinfile` module proves invaluable.

Unlike modules that replace entire files, `lineinfile` allows you to make
surgical modifications to specific lines while preserving the rest of the file's
content. This targeted approach is perfect for updating configuration
parameters, adding entries to system files, or toggling settings without
disrupting the file's overall structure.

Consider scenarios where you need to change a single parameter in a large
configuration file, add a new entry to your hosts file, or comment out a
deprecated setting. These tasks require precision—changing too little means the
task isn't completed, while changing too much risks breaking functionality. The
`lineinfile` module excels in these situations by focusing changes only where
needed.

In this guide, we'll explore the capabilities of the `lineinfile` module and
learn how to use it effectively for various configuration management tasks on
your local machine.

<iframe width="100%" height="315" src="https://www.youtube.com/embed/cwa_MBL1kek" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

## Understanding the lineinfile module

At its core, the `lineinfile` module ensures that a particular line exists in a
file in a specific form, or that it doesn't exist at all. It works by searching
for patterns using regular expressions and then applying the appropriate action.

### Core parameters

The power of `lineinfile` comes from its flexible parameters:

- `path` (or `dest`) specifies the file you want to modify. This is the only
  required parameter and tells Ansible which file to work with.

- `line` contains the content you want to add or use as a replacement. This is
  what will appear in the file after the module runs.

- `regexp` defines the pattern to match. The module uses this regular expression
  to find lines that need to be changed or to determine where to add new
  content.

- `state` determines whether the specified line should be present or absent in
  the file. By default, it's set to "present."

- `insertafter` and `insertbefore` control where a new line should be added if
  it doesn't already exist in the file. You can specify a regex pattern or use
  special values like "BOF" (beginning of file) or "EOF" (end of file).

- `backup` creates a backup of the original file before making changes, which is
  especially valuable when modifying critical system files.

### When to use lineinfile

The `lineinfile` module shines in specific scenarios but isn't always the best
choice. It's ideal for:

- Making small, targeted changes to configuration files
- Ensuring specific settings are configured correctly
- Adding or removing individual entries
- Working with well-structured files where lines are clearly identifiable

However, for more complex requirements, consider alternatives:

- For multi-line changes, the `blockinfile` module offers better control
- When managing entire configuration files with many interdependent settings,
  the `template` module provides a more holistic approach
- For structured data formats like YAML or JSON, specialized modules will handle
  the format's syntax rules correctly

Let's see a simple example of the `lineinfile` module in action:

```yaml
[label basic_example.yml]
- name: Basic lineinfile example
  hosts: localhost
  tasks:
    - name: Add a line to a file
      ansible.builtin.lineinfile:
        path: /tmp/example.txt
        line: 'This line will be added to the file'
        create: yes
```

This straightforward playbook adds a line to a text file, creating the file if
it doesn't exist.

To run this playbook, save it as `basic_example.yml` and execute the following
command in your terminal:

```command
ansible-playbook basic_example.yml
```

You should see output similar to:

```text
[output]
PLAY [Basic lineinfile example] *********************************************

TASK [Gathering Facts] ******************************************************
ok: [localhost]

TASK [Add a line to a file] *************************************************
changed: [localhost]

PLAY RECAP ******************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

After the playbook runs, you can verify the result by examining the file:

```command
cat /tmp/example.txt
```

You should see:

```text
[output]
This line will be added to the file
```

If you run the playbook a second time, the task will report `ok` instead of
`changed` because the line already exists, demonstrating Ansible's idempotent
behavior.

## Basic lineinfile operations

Now let's explore some fundamental operations you can perform with the
`lineinfile` module. These form the building blocks for more complex
configuration tasks.

### Replacing lines that match a pattern

Often, you'll need to find an existing line and replace it with updated content.
This is where the `regexp` parameter becomes crucial:

```yaml
[label replace_line.yml]
- name: Replace a line in a configuration file
  hosts: localhost
  tasks:
    - name: Create a sample configuration file
      ansible.builtin.copy:
        dest: /tmp/config.ini
        content: |
          # Sample configuration file
          server_port = 8080
          debug_mode = false
          log_level = info

[highlight]
    - name: Update the server port
      ansible.builtin.lineinfile:
        path: /tmp/config.ini
        regexp: ^server_port = .*
        line: server_port = 9090
[/highlight]
```

This task searches for any line that starts with "server_port = " and replaces
it with the new value. Since the regular expression matches any value, this
configuration allows for consistent updates regardless of the file's initial
state.

To execute the playbook, run:

```command
ansible-playbook replace_line.yml
```

```text
[output]
PLAY [Replace a line in a configuration file] *******************************

TASK [Gathering Facts] ******************************************************
ok: [localhost]

TASK [Create a sample configuration file] ***********************************
changed: [localhost]

TASK [Update the server port] ***********************************************
changed: [localhost]

PLAY RECAP ******************************************************************
localhost                  : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

Examine the modified file:

```command
cat /tmp/config.ini
```

Output:

```text
[output]
# Sample configuration file
server_port = 9090
debug_mode = false
log_level = info
```

Notice that only the `server_port` line has changed, while the rest of the file
remains untouched.

### Removing lines from files

Sometimes you need to remove configuration entries entirely, such as disabling
deprecated settings:

```yaml
[label remove_line.yml]
- name: Remove a line from a file
  hosts: localhost
  tasks:
    - name: Create a sample file with multiple lines
      ansible.builtin.copy:
        dest: /tmp/sample.txt
        content: |
          Line 1: Keep this line
          Line 2: This line should be removed
          Line 3: Keep this line too

[highlight]
    - name: Remove the unwanted line
      ansible.builtin.lineinfile:
        path: /tmp/sample.txt
        regexp: ^Line 2:.*
        state: absent
[/highlight]
```

By setting `state: absent`, you instruct Ansible to remove any line matching the
pattern. This is perfect for cleaning up configuration files by removing
outdated or unnecessary settings.

Once you execute the playbook:

```command
ansible-playbook remove_line.yml
```

You will see the following output:

```text
[output]
PLAY [Remove a line from a file] ********************************************

TASK [Gathering Facts] ******************************************************
ok: [localhost]

TASK [Create a sample file with multiple lines] *****************************
changed: [localhost]

TASK [Remove the unwanted line] *********************************************
changed: [localhost]

PLAY RECAP ******************************************************************
localhost                  : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

Examine the file to confirm the line was removed:

```command
cat /tmp/sample.txt
```

```text
[output]
Line 1: Keep this line
Line 3: Keep this line too
```

The second line has been removed while preserving the rest of the file's
content.

### Working with file permissions

When modifying system files, proper permissions are critical. The `lineinfile`
module allows you to set permissions as part of the modification:

```yaml
[label set_permissions.yml]
- name: Add a line and set proper permissions
  hosts: localhost
  tasks:
    - name: Add a line and set proper permissions
      ansible.builtin.lineinfile:
        path: /tmp/secure_config.txt
        line: secure_setting = true
        owner: "{{ ansible_user_id }}"
        group: "{{ ansible_user_id }}"
        mode: 0644
        create: yes
```

This task not only adds a line to the file but also sets the file ownership to
the current user and applies appropriate permissions—read and write for the
owner, read-only for everyone else.

#### Running set_permissions.yml

Execute the playbook:

```command
ansible-playbook set_permissions.yml
```

```text
[output]
PLAY [Add a line and set proper permissions] ********************************

TASK [Gathering Facts] ******************************************************
ok: [localhost]

TASK [Add a line and set proper permissions] ********************************
changed: [localhost]

PLAY RECAP ******************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

Verify the file content and permissions:

```command
cat /tmp/secure_config.txt
ls -l /tmp/secure_config.txt
```

Output:

```text
[output]
secure_setting = true
-rw-r--r-- 1 yourusername yourusername 21 Mar 19 10:15 /tmp/secure_config.txt
```

The file has been created with the specified content and permissions.

## Using regular expressions with lineinfile

Regular expressions are the secret to the `lineinfile` module's power, allowing
for precise targeting of content within files.

### Understanding regex patterns for lineinfile

Regular expressions provide a language for pattern matching in text. With
`lineinfile`, you use these patterns to identify exactly which lines to modify.

For example, to uncomment a configuration line:

```yaml
[label uncomment_line.yml]
- name: Uncomment a configuration setting
  hosts: localhost
  tasks:
    - name: Create a sample configuration with commented line
      ansible.builtin.copy:
        dest: /tmp/app.conf
        content: |
          # Application settings
          app_name = MyApp
          # debug_mode = false
          log_level = info

[highlight]
    - name: Uncomment and update debug mode
      ansible.builtin.lineinfile:
        path: /tmp/app.conf
        regexp: ^#\s*debug_mode\s*=
        line: debug_mode = true
[/highlight]
```

This task finds any line that starts with a hash followed by optional
whitespace, then `debug_mode` and equals sign (a commented-out setting), and
replaces it with an active setting.

You can execute the playbook:

```command
ansible-playbook uncomment_line.yml
```

```text
[output]
PLAY [Uncomment a configuration setting] ************************************

TASK [Gathering Facts] ******************************************************
ok: [localhost]

TASK [Create a sample configuration with commented line] ********************
changed: [localhost]

TASK [Uncomment and update debug mode] **************************************
changed: [localhost]

PLAY RECAP ******************************************************************
localhost                  : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

The examine the modified file:

```command
cat /tmp/app.conf
```

You should see:

```text
[output]
# Application settings
app_name = MyApp
debug_mode = true
log_level = info
```

Notice that the commented debug_mode line has been uncommented and its value
changed to `true`.

### Capturing groups and backreferences

When you need to preserve parts of the original line while changing others,
capturing groups and backreferences become invaluable:

```yaml
[label backreferences.yml]
- name: Use backreferences to preserve formatting
  hosts: localhost
  tasks:
    - name: Create a sample formatted file
      ansible.builtin.copy:
        dest: /tmp/formatted_config.txt
        content: |
          # Settings with specific formatting
          MAX_CONNECTIONS     =     10
          TIMEOUT             =     30
          RETRY_COUNT         =     3

    - name: Update connections while preserving format
      ansible.builtin.lineinfile:
        path: /tmp/formatted_config.txt
        regexp: '^(MAX_CONNECTIONS\s*=\s*).*'
        line: '\1100'
        backrefs: yes
```

In this example, the regular expression captures the `MAX_CONNECTIONS = ` part
with exact spacing in a group. The `\1` in the replacement line references this
captured group, preserving the exact formatting while changing only the value.
The `backrefs: yes` parameter is necessary to enable this functionality.

Once the playbook is run:

```command
ansible-playbook backreferences.yml
```

```text
[output]
PLAY [Use backreferences to preserve formatting] ****************************

TASK [Gathering Facts] ******************************************************
ok: [localhost]

TASK [Create a sample formatted file] ***************************************
changed: [localhost]

TASK [Update connections while preserving format] ***************************
changed: [localhost]

PLAY RECAP ******************************************************************
localhost                  : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

You can examine the modified file:

```command
cat /tmp/formatted_config.txt
```

It should display:

```text
[output]
# Settings with specific formatting
MAX_CONNECTIONS     =     100
TIMEOUT             =     30
RETRY_COUNT         =     3
```

Notice how the exact spacing around the equals sign has been preserved, while
only the value has been updated to `100`.

### Testing regex patterns

When working with complex regex patterns, testing before implementation is
prudent:

```yaml
[label test_regex.yml]
- name: Test a regex pattern before applying
  hosts: localhost
  tasks:
    - name: Create a test configuration
      ansible.builtin.copy:
        dest: /tmp/test.conf
        content: |
          # Server configuration
          Listen 8080
          DocumentRoot /var/www

    - name: Test a regex pattern (dry run)
      ansible.builtin.lineinfile:
        path: /tmp/test.conf
        regexp: '^(\s*Listen\s+)\d+'
        line: '\180'
        backrefs: yes
[highlight]
        check_mode: yes
[/highlight]
      register: result

    - name: Show what would change
      ansible.builtin.debug:
        var: result
```

The `check_mode: yes` parameter performs a dry run without making actual
changes. The result is registered and can be examined to ensure the pattern
works as expected before applying changes to production files.

#### Running test_regex.yml

Execute the playbook:

```command
ansible-playbook test_regex.yml
```

Output:

```text
[output]
PLAY [Test a regex pattern before applying] *********************************

TASK [Gathering Facts] ******************************************************
ok: [localhost]

TASK [Create a test configuration] ******************************************
changed: [localhost]

TASK [Test a regex pattern (dry run)] ***************************************
changed: [localhost]

TASK [Show what would change] ***********************************************
ok: [localhost] => {
    "result": {
        "changed": true,
        "diff": [
            {
                "after": "Listen 80",
                "after_header": "new line",
                "before": "Listen 8080",
                "before_header": "line"
            }
        ],
        "failed": false,
        "msg": "line replaced"
    }
}

PLAY RECAP ******************************************************************
localhost                  : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

The debug output shows what would change if the task were run normally. In this
case, `Listen 8080` would be replaced with `Listen 80`. The file remains
unchanged because of check mode, which you can verify:

```command
cat /tmp/test.conf
```

Output:

```text
[output]
# Server configuration
Listen 8080
DocumentRoot /var/www
```

[ad-logs]

## Practical example: Editing configuration files

Let's apply these concepts to a practical scenario: configuring a web server on
your local machine for development purposes.

First, we'll create a sample configuration file and then modify it to suit our
development needs:

```yaml
[label web_server_config.yml]
- name: Configure local web server
  hosts: localhost
  vars:
    config_file: /tmp/webserver.conf
    port: 8080
    document_root: /tmp/www

  tasks:
    - name: Create base configuration
      ansible.builtin.copy:
        dest: "{{ config_file }}"
        content: |
          # Web Server Configuration

          Listen 80
          DocumentRoot /var/www/html

          LogLevel warn
          ErrorLog logs/error.log
          CustomLog logs/access.log combined

          # Security settings
          # ServerTokens Prod
          # ServerSignature Off

    - name: Update listening port
      ansible.builtin.lineinfile:
        path: "{{ config_file }}"
        regexp: '^Listen\s+\d+'
        line: "Listen {{ port }}"

    - name: Set document root for local development
      ansible.builtin.lineinfile:
        path: "{{ config_file }}"
        regexp: '^DocumentRoot\s+'
        line: "DocumentRoot {{ document_root }}"

    - name: Enable security settings (uncomment)
      ansible.builtin.lineinfile:
        path: "{{ config_file }}"
        regexp: '^#\s*(ServerTokens\s+Prod)'
        line: '\1'
        backrefs: yes

    - name: Create document root directory
      ansible.builtin.file:
        path: "{{ document_root }}"
        state: directory

    - name: Display the modified configuration
      ansible.builtin.command: cat {{ config_file }}
      register: cat_result
      changed_when: false

    - name: Show the final configuration
      ansible.builtin.debug:
        var: cat_result.stdout_lines
```

Execute the playbook:

```command
ansible-playbook web_server_config.yml
```

```text
[output]
PLAY [Configure local web server] *******************************************

TASK [Gathering Facts] ******************************************************
ok: [localhost]

TASK [Create base configuration] ********************************************
changed: [localhost]

TASK [Update listening port] ************************************************
changed: [localhost]

TASK [Set document root for local development] ******************************
changed: [localhost]

TASK [Enable security settings (uncomment)] *********************************
changed: [localhost]

TASK [Create document root directory] ***************************************
changed: [localhost]

TASK [Display the modified configuration] ***********************************
changed: [localhost]

TASK [Show the final configuration] *****************************************
ok: [localhost] => {
    "cat_result.stdout_lines": [
        "# Web Server Configuration",
        "",
        "Listen 8080",
        "DocumentRoot /tmp/www",
        "",
        "LogLevel warn",
        "ErrorLog logs/error.log",
        "CustomLog logs/access.log combined",
        "",
        "# Security settings",
        "ServerTokens Prod",
        "# ServerSignature Off"
    ]
}

PLAY RECAP ******************************************************************
localhost                  : ok=8    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

The final output shows the modified configuration with:

1. The port changed from 80 to 8080
2. The document root updated to /tmp/www
3. The ServerTokens line uncommented

Additionally, the playbook created the document root directory to ensure the
configuration would work if implemented.

## Managing application properties

Many applications use properties files for configuration. Let's see how
`lineinfile` can manage these types of files:

```yaml
[label app_properties.yml]
- name: Configure application properties
  hosts: localhost
  vars:
    properties_file: /tmp/application.properties
    db_url: "jdbc:mysql://localhost:3306/devdb"
    log_level: "debug"

  tasks:
    - name: Create properties file
      ansible.builtin.copy:
        dest: "{{ properties_file }}"
        content: |
          # Application properties
          app.name=MyApp
          app.version=1.0

          # Database settings
          spring.datasource.url=jdbc:h2:mem:testdb
          spring.datasource.username=sa
          spring.datasource.password=

          # Logging
          logging.level.root=info
          logging.file=/tmp/app.log

    - name: Update database connection
      ansible.builtin.lineinfile:
        path: "{{ properties_file }}"
        regexp: '^spring\.datasource\.url='
        line: "spring.datasource.url={{ db_url }}"

    - name: Set logging level
      ansible.builtin.lineinfile:
        path: "{{ properties_file }}"
        regexp: '^logging\.level\.root='
        line: "logging.level.root={{ log_level }}"

    - name: Display the modified properties
      ansible.builtin.command: cat {{ properties_file }}
      register: cat_result
      changed_when: false

    - name: Show the final properties
      ansible.builtin.debug:
        var: cat_result.stdout_lines
```

Execute the playbook:

```command
ansible-playbook app_properties.yml
```

Output:

```text
[output]
PLAY [Configure application properties] *************************************

TASK [Gathering Facts] ******************************************************
ok: [localhost]

TASK [Create properties file] ***********************************************
changed: [localhost]

TASK [Update database connection] *******************************************
changed: [localhost]

TASK [Set logging level] ****************************************************
changed: [localhost]

TASK [Display the modified properties] **************************************
changed: [localhost]

TASK [Show the final properties] ********************************************
ok: [localhost] => {
    "cat_result.stdout_lines": [
        "# Application properties",
        "app.name=MyApp",
        "app.version=1.0",
        "",
        "# Database settings",
        "spring.datasource.url=jdbc:mysql://localhost:3306/devdb",
        "spring.datasource.username=sa",
        "spring.datasource.password=",
        "",
        "# Logging",
        "logging.level.root=debug",
        "logging.file=/tmp/app.log"
    ]
}

PLAY RECAP ******************************************************************
localhost                  : ok=6    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

The final properties file shows:

1. The database URL has been changed from an in-memory H2 database to a MySQL
   connection.
2. The [logging level](https://betterstack.com/community/guides/logging/log-levels-explained/) has been updated from `info` to
   `debug`.

This example demonstrates how `lineinfile` can update Java properties files,
which are common in many enterprise applications.

## Advanced techniques with lineinfile

As you become comfortable with basic `lineinfile` operations, you can explore
more advanced techniques to handle complex scenarios.

### Conditional modifications

Sometimes you need to make changes based on the current state of the file or
other conditions:

```yaml
[label conditional.yml]
- name: Make conditional changes
  hosts: localhost
  vars:
    config_file: /tmp/conditional.conf

  tasks:
    - name: Create initial configuration
      ansible.builtin.copy:
        dest: "{{ config_file }}"
        content: |
          # Environment configuration
          ENV=production
          LOG_LEVEL=normal
          DEBUG=false

    - name: Check if production mode is enabled
      ansible.builtin.command: grep -q "^ENV=production" {{ config_file }}
      register: grep_result
      changed_when: false
      failed_when: false

    - name: Enable detailed logging in production mode
      ansible.builtin.lineinfile:
        path: "{{ config_file }}"
        regexp: '^LOG_LEVEL='
        line: 'LOG_LEVEL=verbose'
      when: grep_result.rc == 0

    - name: Display the modified configuration
      ansible.builtin.command: cat {{ config_file }}
      register: cat_result
      changed_when: false

    - name: Show the final configuration
      ansible.builtin.debug:
        var: cat_result.stdout_lines
```

When you execute the playbook:

```command
ansible-playbook conditional.yml
```

You will see:

```text
[output]
PLAY [Make conditional changes] *********************************************

TASK [Gathering Facts] ******************************************************
ok: [localhost]

TASK [Create initial configuration] *****************************************
changed: [localhost]

TASK [Check if production mode is enabled] **********************************
changed: [localhost]

TASK [Enable detailed logging in production mode] ***************************
changed: [localhost]

TASK [Display the modified configuration] ***********************************
changed: [localhost]

TASK [Show the final configuration] *****************************************
ok: [localhost] => {
    "cat_result.stdout_lines": [
        "# Environment configuration",
        "ENV=production",
        "LOG_LEVEL=verbose",
        "DEBUG=false"
    ]
}

PLAY RECAP ******************************************************************
localhost                  : ok=6    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

Since the configuration has `ENV=production`, the conditional task executes and
changes the `LOG_LEVEL` to `verbose`. This demonstrates how to make
configuration changes based on the context or content of the file.

### Creating backups before changes

When modifying critical files, creating backups is a prudent safety measure that
can also be achieved with the lineinfile module:

```yaml
[label backup.yml]
---
- name: Modify with backup
  hosts: localhost
  tasks:
    - name: Create important configuration
      ansible.builtin.copy:
        dest: /tmp/important.conf
        content: |
          # Critical system configuration
          # Do not modify manually

          CRITICAL_SETTING=default_value
          SYSTEM_MODE=normal

    - name: Update important setting with backup
      ansible.builtin.lineinfile:
        path: /tmp/important.conf
        regexp: '^CRITICAL_SETTING='
        line: 'CRITICAL_SETTING=new_value'
        backup: yes
      register: backup_result

    - name: Display backup file path
      ansible.builtin.debug:
        msg: "Backup created at: {{ backup_result.backup_file }}"
      when: backup_result.changed

    - name: Compare original and modified files
      ansible.builtin.command: diff {{ backup_result.backup_file }} /tmp/important.conf
      register: diff_result
      changed_when: false
      failed_when: false
      when: backup_result.changed

    - name: Show diff
      ansible.builtin.debug:
        var: diff_result.stdout_lines
      when: backup_result.changed
```

Execute the playbook:

```command
ansible-playbook backup.yml
```

Output:

```text
[output]
PLAY [Modify with backup] ***************************************************

TASK [Gathering Facts] ******************************************************
ok: [localhost]

TASK [Create important configuration] ***************************************
changed: [localhost]

TASK [Update important setting with backup] *********************************
changed: [localhost]

TASK [Display backup file path] *********************************************
ok: [localhost] => {
    "msg": "Backup created at: /tmp/important.conf.22785.2025-03-19@10:30:17~"
}

TASK [Compare original and modified files] **********************************
changed: [localhost]

TASK [Show diff] ************************************************************
ok: [localhost] => {
    "diff_result.stdout_lines": [
        "4c4",
        "< CRITICAL_SETTING=default_value",
        "---",
        "> CRITICAL_SETTING=new_value"
    ]
}

PLAY RECAP ******************************************************************
localhost                  : ok=6    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

The `backup: yes` parameter creates a timestamped copy of the original file
before making changes. The playbook then shows the backup file path and displays
the differences between the original and modified files, confirming that only
the CRITICAL_SETTING line was changed.

### Working with multiple lines

While `lineinfile` operates on one line at a time, you can use it in a loop to
modify multiple lines efficiently:

```yaml
[label multiple_lines.yml]
---
- name: Update multiple settings
  hosts: localhost
  vars:
    config_file: /tmp/multi.conf
    settings:
      - { key: "PORT", value: "8080" }
      - { key: "DEBUG", value: "true" }
      - { key: "LOG_LEVEL", value: "debug" }

  tasks:
    - name: Create initial configuration
      ansible.builtin.copy:
        dest: "{{ config_file }}"
        content: |
          # Multi-line configuration example
          PORT=80
          DEBUG=false
          LOG_LEVEL=info

    - name: Update each setting
      ansible.builtin.lineinfile:
        path: "{{ config_file }}"
        regexp: '^{{ item.key }}='
        line: '{{ item.key }}={{ item.value }}'
      loop: "{{ settings }}"

    - name: Display the final configuration
      ansible.builtin.command: cat {{ config_file }}
      register: cat_result
      changed_when: false

    - name: Show the final settings
      ansible.builtin.debug:
        var: cat_result.stdout_lines
```

#### Running multiple_lines.yml

Execute the playbook:

```command
ansible-playbook multiple_lines.yml
```

Output:

```text
[output]
PLAY [Update multiple settings] *********************************************

TASK [Gathering Facts] ******************************************************
ok: [localhost]

TASK [Create initial configuration] *****************************************
changed: [localhost]

TASK [Update each setting] **************************************************
changed: [localhost] => (item={'key': 'PORT', 'value': '8080'})
changed: [localhost]

TASK [Update each setting] **************************************************
changed: [localhost] => (item={'key': 'PORT', 'value': '8080'})
changed: [localhost] => (item={'key': 'DEBUG', 'value': 'true'})
changed: [localhost] => (item={'key': 'LOG_LEVEL', 'value': 'debug'})

TASK [Display the final configuration] **************************************
changed: [localhost]

TASK [Show the final settings] **********************************************
ok: [localhost] => {
    "cat_result.stdout_lines": [
        "# Multi-line configuration example",
        "PORT=8080",
        "DEBUG=true",
        "LOG_LEVEL=debug"
    ]
}

PLAY RECAP ******************************************************************
localhost                  : ok=5    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

This approach uses a variable to define multiple settings and their values, then
loops through them to update each one in the configuration file. The final
output shows all three settings successfully updated according to the values
defined in the `settings` variable.

## Some lineinfile best practices

As you work with the `lineinfile` module, keep the following best practices in
mind:

### 1. Test before applying

Always test your regular expressions on sample files before applying them to
production systems. The `check_mode` parameter is invaluable for seeing what
would change without making actual modifications.

For example, you can use:

```command
ansible-playbook your_playbook.yml --check
```

This runs the entire playbook in check mode, showing what would change without
making actual modifications.

### 2. Be specific with patterns

Make your regular expressions as specific as possible to avoid unintended
matches. For example, using `^setting=` rather than just `setting=` ensures you
only match lines that begin with that setting.

Consider this example:

```yaml
# Too general - might match in comments or other contexts
regexp: 'port='

# More specific - only matches lines starting with "port="
regexp: '^port='

# Even more specific - ensures proper formatting
regexp: '^port\s*=\s*\d+'
```

### 3. Create backups

Use the `backup: yes` parameter when modifying critical files to create
automatic backups before changes are applied. This provides a safety net if
something goes wrong.

### 4. Ensure idempotence

Design your tasks to be idempotent—running the same playbook multiple times
should produce the same result without unnecessary changes. This is a core
principle of Ansible and ensures predictable behavior.

Test your playbooks by running them multiple times to verify they only make
changes when needed.

### 5. Know when to use alternatives

While `lineinfile` is powerful, recognize when other modules might be more
appropriate:

- Use `blockinfile` for multi-line changes.
- Use `template` for managing complete files with complex structures.
- Use format-specific modules for structured data like YAML or JSON.

## Final thoughts

The `lineinfile` module is a powerful tool in the Ansible ecosystem, enabling
precise control over configuration files with minimal impact. By mastering
regular expressions and understanding the module's parameters, you can automate
complex configuration tasks across multiple systems while maintaining
consistency and reliability.

Throughout this guide, we've explored how to use `lineinfile` for various tasks:
adding lines, replacing content, removing entries, and preserving formatting.
We've seen how to work with different types of configuration files and how to
build more complex automation workflows by combining multiple `lineinfile`
tasks.

The focus on local development environments demonstrates how you can use these
techniques in a practical, low-risk way before applying them to production
systems. The same principles apply whether you're managing a single development
machine or a fleet of production servers.

Thanks for reading!