package Tie::File::Hashify;

use strict;
use warnings;

use Carp;
use IO::File;

our $VERSION = '0.03';

	my ($class, $path, %options) = @_;

	my $self = bless {
		hash => {},
		format => $options{format},
		parse => $options{parse},
		path => $path,
		ro => $options{ro},
		dirty => 0,
	}, $class;

	if($path and -e $path and $options{parse}) {
		my $io = new IO::File($path) or croak "Can't read $path. $!.\n";

		while(my $line = $io->getline) {
			next unless defined $line and length($line);

			my ($key, $value);

			# Use callback for parsing.
			if(ref($options{parse}) eq 'CODE') {
				($key, $value) = &{$options{parse}}($line);

			# Parse line using a regular expression.
			elsif(ref($options{parse}) eq '' or uc(ref($options{parse})) eq 'REGEXP') {
				my $re = ref($options{parse}) ? $options{parse} : qr/^$options{parse}$/;
				($key, $value) = ($line =~ $re);

			# Croak.
			else {
				croak 'Can\'t use ', lc(ref($options{parse})), " for parsing.\n";

			if(defined $key and length $key) {
				$self->{hash}->{$key} = $value if(length $key);


	return $self;

sub FETCH {
	my ($self, $key) = @_;
	return $self->{hash}->{$key};

sub STORE {
	my ($self, $key, $value) = @_;

	croak "Can't store in read-only mode" if($self->{ro});

	$self->{dirty} = !0;

	return $self->{hash}->{$key} = $value;

sub EXISTS {
	my ($self, $key) = @_;
	return exists($self->{hash}->{$key});

sub DELETE {
	my ($self, $key) = @_;

	croak "Can't delete in read-only mode" if($self->{ro});

	$self->{dirty} = !0;

	return delete($self->{hash}->{$key});

sub CLEAR {
	my ($self) = @_;

	croak "Can't clear in read-only mode" if($self->{ro});

	$self->{dirty} = !0;

	%{$self->{hash}} = ();

	my ($self) = @_;
	my ($key) = each %{$self->{hash}};
	return $key;

	my ($self) = @_;

	my ($k, $v) = each %{$self->{hash}};

	return $k;

sub SCALAR {
	my ($self) = @_;

	my $format = $self->{format};

	if(defined $format) {
		my $text = '';

		values %{$self->{hash}};

		while(my ($key, $value) = each %{$self->{hash}}) {
			# Format using callback.
			if(ref($format) eq 'CODE') {
				$text .= &{$format}($key, $value) . "\n";

			# Format using sprintf and a format string.
			elsif(ref($format) eq '') {
				$text .= sprintf($format, $key, $value) . "\n";

			# Croak.
			else {
				croak 'Can\'t use ' . ref($format) . " as format.\n";

		return $text;

	else {
		return %{$self->{hash}};

sub _store {
	my ($self) = @_;

	my $path = $self->{path};

	if($path and $self->{dirty} and $self->{format} and !$self->{ro}) {
		my $io = new IO::File('>' . $path) or croak "Can't write $path. $!.\n";


		$self->{dirty} = 0;

sub UNTIE {
	my ($self) = @_;


	my ($self) = @_;




=head1 NAME

TIe::File::Hashify - Parse a file and tie the result to a hash.


	use Tie::File::Hashify;

	my %rc;
	my $path = "$ENV{HOME}/.some.rc";

	# Parse lines like 'foo = bar':
	sub parse { $_[0] =~ /^\s*(\S+)\s*=\s*(.*?)\s*$/ };

	# Format pairs as 'key = value':
	sub format { "$_[0] = $_[1]" };

	tie(%rc, 'Tie::File::Hashify', $path, parse => \&parse, format => \&format);

	print "option 'foo' = $rc{foo}\n";

	# Add new option.
	$rc{bar} = 'moo';

	# Save file.
	untie %rc;


This module helps parsing simple text files and mapping it's contents to a
plain hash. It reads a file line by line and uses a callback or expression you
provide to parse a key and a value from it. The key/value pairs are then
available through the generated hash. You can also provide another callback or
format string that formats a key/value pair to a line to be stored back to the

=head1 METHODS

=over 4

=item B<tie>(%hash, 'Tie::File::Hashify', $path, %options)

The third argument (after the hash itself and the package name of course) is
the path to a file. The file does not really have to exist, but using a path to
a non-existent file does only make sense if you provide a format-callback to
write a new file.

After the second argument, a list of options may/should follow:

=over 4

=item B<parse>

Either a code reference, which will be called with a line as argument and
should return the key and the value for the hash element; or a string or
compiled regular expression (qr//). The expression will be applied to every
line and $1 and $2 will be used as key/value afterwards.

=item B<format>

This is used for formatting the hash into something that can be written back to
the file. It may be a code reference that takes two arguments (key and value)
as arguments and returns a string (without trailing line-break - it will be
added automatically), or a format string that is forwarded to B<sprintf>
together with the key and the value.

=item B<ro>

If this is true, changing the hash will make it croak, and the content will not
be written back to the file.


All arguments are optional. If you don't give any arugments, you get a plain
normal hash.



Copyright (C) 2008 by Jonas Kramer <>. Published under the
terms of the Artistic License 2.0.

"read-only" functionality by Marco Emilio Poleggi.