Skip to main content

Poor man's Ansible: lineinfile and blockinfile in bash

·3 mins
Kristof Kovacs
Author
Kristof Kovacs
Software Architect & DevOps Consultant

Hello, I’m Kristof, a human being like you, and an easy to work with, friendly guy.

I've been a programmer, a consultant, CIO in startups, head of software development in government, and built two software companies.

Some days I’m coding Golang in the guts of a system and other days I'm wearing a suit to help clients with their DevOps practices.

Table of Contents

Two of my favorite functions in Ansible are lineinfile and blockinfile. They are extraordinarily useful when one needs to ensure that a line or a block is either replaced or put in a config file.

lineinfile #

For example, let's say one wants to enable IP forwarding in the sysctl, one can write the following task in Ansible:

- name: Enable IP forwarding
  lineinfile:
    dest: /etc/sysctl.conf
    regexp: "net.ipv4.ip_forward"
    line: "net.ipv4.ip_forward=1"
    state: present

What exactly this task does is:

  1. Looks for a line containing net.ipv4.ip_forward in the file /etc/sysctl.conf.
  2. If it finds such a line, it replaces that line with net.ipv4.ip_forward=1.
  3. But if such a line was not yet found in the file, then it appends net.ipv4.ip_forward=1 to the end of the file.

When this has ran, the required setting is guaranteed to be in the config file: either at the end, or the previous similar line was replaced. It's much more elegant than just appending, which would get added as many times as the script has ran.

Warning! The following code is ugly and hard to debug. Thus, it is not for use in public scripts, this is more for injection.

But sometimes one is writing simpler (bash) scripts, and want the same functionality. One way to replicate this behaviour would be with a sed command to replace, then a grep to see if the replacement was successful, and then a echo >>... if it was not.

But, here's this "beautiful" sed one-liner:

function lineinfile() { line=${2//\//\\/} ; sed -i -e '/'"${1//\//\\/}"'/{s/.*/'"${line}"'/;:a;n;ba;q};$a'"${line}" "$3" ; }

(They say that perl is a write-only language, but apparently sed can be that too. 😀 You can also find these snippets on github amongst my conf files.)

Usage:

lineinfile 'net.ipv4.ip_forward' 'net.ipv4.ip_forward=1' /etc/sysctl.conf

Let's look at each part:

  • line=${2//\//\\/} => Replaces /-s with \/ so one can use slash.

  • sed -i -e '/'"${1//\//\\/}"'/{...}" "$3" ; } => runs the sed command on the file in $3, in-place. Escapes slashes in $1.

  • s/.*/'"${line}"'/" => If a match for our regexp was found by the previous command, then this part substitutes (s) the whole line (.*) with the new version ($line). Fairly straightforward.

  • :a;n;ba;q" => Now this is tricky. After the replacement, we completely change how sed works: the :a;n;ba part takes over, and in an infinite loop, defines a label (:a), reads a line and prints it (n), then jumps back to the label a (ba). And if it can't read any more lines then ba doesn't just anymore, and then it quits (q). This prevents the running of the next section.

  • $a'"${line}" => This appends (a) the line at the end of the file ($). But, if the pattern was found, then the q after our read-print loop never lets this run.

For example usage, you can see my inject script to quickly set up history config in .bashrc:

lineinfile 'HISTFILESIZE=' 'HISTFILESIZE=10000' ~/.bashrc
lineinfile 'HISTTIMEFORMAT=' 'HISTTIMEFORMAT="%F %T "' ~/.bashrc

blockinfile #

Along similar lines, here's the blockinfile one-liner:

function blockinfile() { sed -i -ne '/'"${1//\//\\/}"'/{r/dev/stdin' -e ':a;n;/'"${2//\//\\/}"'/{:b;n;p;bb};ba};p;$r/dev/stdin' "$3" ; }

Usage:

blockinfile STARTMARK ENDMARK filename <<EOF
# STARTMARK
some text to
put inside
# ENDMARK
EOF

This works along very similar lines as the previous code. The only thing worth adding is that the additional -e in the middle, after the filename, is there to separate the filename from the rest of the code (sed is not that smart this way).