Poor man's Ansible: lineinfile and blockinfile in bash
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:
- Looks for a line containing
net.ipv4.ip_forward
in the file/etc/sysctl.conf
. - If it finds such a line, it replaces that line with
net.ipv4.ip_forward=1
. - 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.
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 howsed
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 labela
(ba
). And if it can't read any more lines thenba
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 theq
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).