Perl's Tie::File Module

Two factors outshine all others as reasons I became a systems administrator. The first is that, well, I like computers and computing. The second is that I'm not particularly fond of writing code. If I were, I'd probably be a programmer. As it stands, if I can find tools that work, I won't write code. However, inevitably, some site-specific situation will come up for which I have no choice. In that case, I at least want to write very little code. Perl's Tie::File has helped me do that on more than one occasion.

I'm sure hardcore Perl hackers could leave some crazy code snippets in the comments for this article as examples of the insanity that Tie::File is capable of. However, in my day-to-day work, I'm dealing with system files. Many of them are configuration files, or flat-file databases of one form or another. Tie::File allows me to act on an entire file line by line, instead of bit by bit. Here's an example.

The big setup

In my environment, we have a research lab in which we use compat mode for account information, and point the machines to an LDAP server outside our domain. Compat mode provides compatibility with + and +@ syntax in the /etc/passwd, /etc/group, and /etc/shadow files. We'll use /etc/passwd in this example. The syntax compat mode supports allows you to add an account to the local system whose information (shell, home directory, and crypted password string, among other things) resides elsewhere, like on a NIS or LDAP server. So I can add a line like this to the end of my /etc/passwd file:

+jonesy

This makes the user jonesy a valid account on the local machine, but I can still maintain the user's account information centrally via NIS or LDAP. Adding a line starting with +@ to my passwd file adds all users belonging to a particular netgroup, instead of just one user. My experience so far, though, is that this only works against a NIS server; the code for LDAP support is presumably on the way.

In my nsswitch.conf file, I need to configure the passwd system database using two lines: one to tell it to use compat mode, and the second to tell the system which service to use to look up information on lines starting with +:

passwd: compat
passwd_compat: ldap

There's a small catch to this, in that I can't just tag account entries to the end of my /etc/passwd file, because the last line of my passwd file has a safety net feature which looks like this:

+::::::/bin/false

This line includes all other valid LDAP accounts, making their login shell /bin/false. This is to keep any system goofiness from letting a user in who has a valid LDAP account, but no + entry in the /etc/passwd file. On some systems this can happen, though the user won't have a home directory, and they'll have the old "I have no name!" in their shell prompt. Therefore this needs to be the last line of the file.

Here's where Tie::File comes in. Here's a part of an adduser script (with line numbers added) that adds a user to the /etc/passwd file just above the last line:

1 #!/usr/bin/perl

2 use Tie::File;

3 tie @rows, 'Tie::File', '/etc/passwd' || die "Can't open: $!\n";
4 $numrows = @rows;
5 $insertpoint = $numrows - 1;
6 $newrec="+$ARGV[0]\n";
7 splice @rows, $insertpoint, 0, $newrec;
8 untie @rows;

If this file is called as adduser jonesy, then it will add the line +jonesy (in the code it's +$ARGV[0]) just above the last line of the passwd file. Line 1 tells the system which interpreter to use for the following code, and line 2 says to load a particular module. In line 3, I bind (or "tie") my array variable @rows to the rows in the /etc/passwd file, using Tie::File. Now I'm free to act upon the rows in this file as individual widgets. I can push new records to the file, or use a regex to determine if a line (in this case, an account) is still valid, and remove it only if it is invalid.

On line 4, I get the number of rows in the file (returned by @rows) and assign it to $numrows. Then I set up my insertion point on line 5 to be the row before the last row. On line 6, I define the record to be inserted as the argument passed on the command line, preceded with a plus-sign. Finally, I use splice on line 7, using the line before the last line as my offset, character 0 as my starting point, and the new record as the value to be spliced in between the last two existing lines of the file. To clean up, I simply untie my array on line 8.

In conclusion

This real-world scenario illustrates an actual useful purpose for the Tie::File module, and it's only one of thousands of possibilities. Think about all of the files systems administrators, or even home users, deal with: iptables scripts, passwd and group files, tcpwrappers files, simple flat-file database files. Tie::Files is an extremely easy-to-use module for performing insert, delete, and modify operations, one line at a time.