Mastering Repetitive Configurations With m4

From CodeCodex

m4 is a tool that reads a stream of text, looking for references to macros, and expands those macros before outputting the text. It is a useful tool for generating highly-repetitive, as for example happens with certain system configuration files.

For example, imagine you’re the sysadmin for a company office. You want to put everyone’s machines into the DHCP server, so that IP address allocations can be managed in one central place, instead of having to individually configure every machine. Each person will also have a SIP phone that connects to your office Asterisk server. Instead of having to update two different config files every time a staff member joins, leaves or moves, why not group the information into a single text file, and use that to generate the appropriate DHCP and Asterisk configuration?

Let’s consider what information we need: a number for the extension, some kind of description of the person/station, the MAC address of their PC, and an IP address to allocate to them. With this, we can already start compiling the information for our staff, in the form of calls to a (yet-to-be-defined) defstation macro:

defstation(1, `Reception', `2E-D9-67-EB-B7-83', 10.1.0.1)
defstation(2, `Sales', `21-68-A3-44-28-5F', 10.1.0.2)
defstation(3, `Accounts', `36-04-2F-98-A1-38', 10.1.0.3)

Suppose this file is called stations. Now, to generate the appropriate DHCP configuration stanzas, let us create a file dhcpd.conf.m4, which contains a suitable definition of defstation, and uses it to process the stations file:

define(`defstation', `
    host $2
      {
        hardware ethernet $3;
        fixed-address $4;
      }')dnl
dnl
include(`stations')dnl

This way, the simple command

m4 dhcpd.conf.m4

will generate the output

    host Reception
      {
        hardware ethernet 2E-D9-67-EB-B7-83;
        fixed-address 10.1.0.1;
      }

    host Sales
      {
        hardware ethernet 21-68-A3-44-28-5F;
        fixed-address 10.1.0.2;
      }

    host Accounts
      {
        hardware ethernet 36-04-2F-98-A1-38;
        fixed-address 10.1.0.3;
      }

Similarly, if a file sip.conf.m4 processes the same stations file through a different definition of defstation:

define(`defstation', `
[phone_`'format(`%02d', $1)]
context=simpleout
type=friend
username=phone_`'format(`%02d', $1)
callerid=("MegaCorp $2 <555-1212 ext eval(7000 + $1)>")
... etc ...')dnl
dnl
include(`stations')dnl

the output will be

[phone_01]
context=simpleout
type=friend
username=phone_01
callerid=("MegaCorp Reception <555-1212 ext 7001>")
... etc ...

[phone_02]
context=simpleout
type=friend
username=phone_02
callerid=("MegaCorp Sales <555-1212 ext 7002>")
... etc ...

[phone_03]
context=simpleout
type=friend
username=phone_03
callerid=("MegaCorp Accounts <555-1212 ext 7003>")
... etc ...

Of course, these are not complete configuration files; there will need to be additional boilerplate stuff before and after these expanded repetitive sections. But this should be enough to give you the general idea: instead of having to hand-edit two files for station changes, you only need to edit one, and use it to generate the actual config files. So much time saved, and less chance of mistakes!