After fiddling for some time with P::RD I sort of got the hang of it. I have a fully functional grammar, and it gives me what I want. However I want to enhance it a bit, and in general I would take any comments/suggestions on how to write a proper grammar. The examples are rather long so I am including them at the end
Thank you in advance for your great help.
Update: Thank you both, you've been rather helpful. It became a monster, but it works extremely well, and seems quite efficient compared to what it can do. I am including what came out of my parser below for google's reference :)
{
use warnings;
use strict;
use Digest::MD5 qw/md5_hex/;
use Data::Dumper;
my $line;
my %quantify = (
s => 1,
m => 60,
h => 3600,
d => 86400,
w => 604800,
);
sub get_duration {
$line = shift;
my $dur = $_[0] * $quantify{$_[1] || 's'};
if ($dur < 60) {
die "Lease durations under one minute are not perm
+itted (line $line)\n";
}
return $dur;
}
sub ip_32 {
return sprintf ('%08b%08b%08b%08b', split (/\./, shift
+));
}
sub ip_ascii {
my @result;
foreach my $ip (@_) {
push @result, join ('.', unpack ('C4', pack ('B*',
+ $ip) ) );
}
return @result;
}
my $parsed_result = {};
my $current = {};
sub add_kv {
$line = shift;
my ($key, $value, $option, $to) = @_;
my $target;
if (! defined $to) {
$target = $parsed_result;
}
elsif ($to eq 'subnet') {
$target = $parsed_result->{subnets}[-1];
}
elsif ($to eq 'soa') {
$target = $parsed_result->{soa} ||= {};
}
else {
die "Unknown add_kv (... to) option '$to'\n";
}
if (exists $target->{$key}) {
die "You can only have one occurence of '$option'
+per section (line $line)\n";
}
$target->{$key} = $value;
return 1;
}
sub add_op {
$line = shift;
my $option = shift;
my $digest = md5_hex ($option);
if (exists $current->{options}{$digest}) {
die "Option '$option' specified twice in the same
+subnet (line $line)\n";
}
push @{$parsed_result->{subnets}[-1]{options}}, $optio
+n;
$current->{options}{$digest} = undef;
return 1;
}
sub add_subnet {
$line = shift;
my ($ip, $netmask) = @_;
if ($netmask !~ /^1*0*$/) {
die "Illegal netmask specified for a subnet at lin
+e $line\n";
}
my $network = "$ip" & "$netmask";
if ($network != $ip) {
warn sprintf ("Warning: you should specify a netwo
+rk number (%s) in a subnet declaration, not a random ip (%s). Fixing\
+n",
ip_ascii ($network, $ip),
);
}
if (my $subs = $parsed_result->{subnets}) {
if (grep { $_->{network} eq $network and $_->{netm
+ask} eq $netmask} (@{$subs}) ) {
die sprintf ("A second declaration of subnet '
+%s' netmask '%s' encountered on line %d\n",
ip_ascii ($network, $netmask),
$line,
);
}
}
push @{$parsed_result->{subnets}}, { network => $netwo
+rk, netmask => $netmask };
$current = {};
return 1;
}
sub adjust {
$current->{$_[0]} = $_[1];
return 1;
}
sub check_map_prereq {
my ($sub, $dn, @ips) = @_;
if ($sub->{dns_update} xor (length ($dn || '') ) ) {
die "Domain name for a mapping must be specified a
+fter a 'dns-update' option (line $line)\n";
}
if ($sub->{dns_update}) {
if (not $parsed_result->{bind_server}) {
die sprintf ("Can not perform requested dns up
+dates for subnet %s/%s without a 'bind-server' specification.\n",
ip_ascii ($sub->{network}, $sub->{netmask}
+),
);
}
if (@ips == 2 and $dn !~ /<addr>/) {
die "Range domain specifications must contain
+an <addr> macro, in order to make the domain name unique (line $line)
+\n";
}
unless (grep { index ($dn, $_) >= 0 } (keys %{$par
+sed_result->{soa}} ) ) {
die "No parent SOA found for the FQDN '$dn'. P
+lease add a proper 'soa' option in the main config section (line $lin
+e).\n";
}
}
foreach my $ip (@ips) {
unless ( ("$ip" & "$sub->{netmask}") eq $sub->{net
+work} ) {
die sprintf ("%s not within current subnet %s/
+%s on line %d\n",
ip_ascii ($ip, $sub->{network}, $sub->{net
+mask}),
$line,
);
}
my $ptr = join ('.', (reverse unpack ('C4', pack (
+'B*', $ip) )), 'in-addr.arpa.');
unless (grep { index ($ptr, $_) >= 0 } (keys %{$pa
+rsed_result->{soa}} ) ) {
die "No parent SOA found for the Reverse DNS F
+QDN '$ptr'. Please add a proper 'soa' to the main config section (lin
+e $line).\n";
}
}
unless ($current->{ttl}) {
die "A 'ttl' option must be specified prior to an
+ip mapping or range declaration (line $line)\n";
}
$current->{max_ttl} ||= $current->{ttl} * 2;
$current->{min_ttl} ||= 60;
if ($current->{ttl} > $current->{max_ttl}) {
$current->{max_ttl} = $current->{ttl} * 2;
warn "'max-ttl' is lower than 'ttl' for mapping at
+ line $line. Bumping up to $current->{max_ttl} seconds.\n";
}
if ($current->{ttl} < $current->{min_ttl}) {
$current->{min_ttl} = $current->{ttl} / 2;
warn "'min-ttl' is higher than 'ttl' for mapping a
+t line $line. Bumping down to $current->{min_ttl} seconds.\n";
}
}
sub make_fqdn {
my ($domain, $dn) = @_;
unless ( (substr $dn, -1) eq '.') {
unless ($domain) {
die "Non-fully qualified domain name at line $
+line not permitted without a prior 'domain' option for the subnet.\n"
+;
}
$dn = join ('.', $dn, $domain);
}
return $dn;
}
sub add_range {
$line = shift;
my ($from, $to, $dn, $op_dn) = @_;
my $subnet = $parsed_result->{subnets}[-1];
if ($subnet->{range}) {
die sprintf ("More than one range statement encoun
+tered for subnet %s/%s (line %d)\n",
ip_ascii ($subnet->{network}, $subnet->{netmas
+k}),
$line,
);
}
if ($from > $to) {
die sprintf ("Invalid range '%s' - '%s' at line %d
+\n",
ip_ascii ($from, $to),
$line,
);
}
$dn = make_fqdn ($current->{domain}, $dn) if (defined
+$dn);
$op_dn = substr ( (make_fqdn ($current->{domain}, $op_
+dn), 0, -1) ) if (defined $op_dn);
check_map_prereq ($subnet, $dn, $from, $to);
$subnet->{range} = {
range => [$from, $to],
$dn ? ( dn => $dn ) : (),
$op_dn ? ( op_dn => $op_dn ) : (),
map { $_, $current->{$_} } qw/ttl max_ttl min_ttl/
+,
};
return 1;
}
sub add_mapping {
$line = shift;
my ($ip, $id_type, $id, $dn, $op_dn) = @_;
my $subnet = $parsed_result->{subnets}[-1];
if (exists $subnet->{maps}{$ip}) {
die sprintf ("Mapping for ip '%s' specified twice
+in subnet %s/%s (line %d)\n",
ip_ascii ($ip, $subnet->{network}, $subnet->{n
+etmask}),
$line,
);
}
if (exists $current->{ids}{$id}) {
die sprintf ("Id '%s' used for more than one mappi
+ng in subnet %s/%s (line %d)\n",
$id,
ip_ascii ($subnet->{network}, $subnet->{netmas
+k}),
$line,
);
}
$dn = make_fqdn ($current->{domain}, $dn) if (defined
+$dn);
$op_dn = substr ( (make_fqdn ($current->{domain}, $op_
+dn), 0, -1) ) if (defined $op_dn);
check_map_prereq ($subnet, $dn, $ip);
$subnet->{maps}{$ip} = {
id_type => $id_type,
id => $id,
defined $dn ? ( dn => $dn) : (),
defined $op_dn ? ( op_dn => $op_dn) : (),
map { $_, $current->{$_} } qw/ttl max_ttl min_ttl/
+,
};
$current->{ids}{$id} = undef;
return 1;
}
sub line_error {
die sprintf ("Unable to parse line %d:\n'%s'\n",
$_[0],
(split (/\n/, $_[1], 2))[0],
);
}
}
nl : / (?: \# [^\n]* )? \n /x # newline with
+ or without preceeding comment
{ $return = 1 }
parse : line(s)
{ $return = $parsed_result }
line : /\z/
| nl
| bind_server
| key
| soa
| subnet
| { line_error ($thisline, $text) }
bind_server : 'bind-server' ip nl
{ add_kv ($prevline, 'bind_server', $item[
+2], 'bind-server') }
key : 'tsig-key' name_str nl
{ add_kv (
$prevline,
'keyfile',
$item[2],
'tsig-key',
) }
soa : 'soa' domain_str nl
{ add_kv (
$prevline,
(substr $item[2], -1) eq '.' ? $item[2
+] : $item[2] . '.',
undef,
'soa',
'soa',
) }
subnet : 'subnet' ip 'netmask' ip
{ add_subnet ($thisline, $item[2], $item[4
+]) }
sub_line(s)
sub_line : nl
| sub_option
| /\z/
sub_option : dns_update
| domain
| option
| ttl
| max_ttl
| min_ttl
| range
| mapping
dns_update : 'dns-update' nl
{ add_kv ($prevline, 'dns_update', 1, 'dns
+-update', 'subnet') }
domain : 'domain' domain_str nl
{ adjust ('domain', (substr $item[2], -1)
+eq '.' ? $item[2] : $item[2] . '.' ) }
option : 'option' /[ \t\w\-"\.\,]+/ nl
{ add_op ( $prevline, $item[2] ) }
ttl : 'ttl' duration nl
{ adjust ('ttl', $item[2] ) }
max_ttl : 'max-ttl' duration nl
{ adjust ('max_ttl', $item[2] ) }
min_ttl : 'min-ttl' duration nl
{ adjust ('min_ttl', $item[2] ) }
range : 'range' ip ip domain_str(0..2) nl
{ add_range ($prevline, @item[2,3], @{$ite
+m[4]} ) }
mapping : ip identifier domain_str(0..2) nl
{ add_mapping (
$prevline,
$item[1],
$item[2][0],
$item[2][1],
@{$item[3]},
) }
host_str : /[a-z][0-9a-z\-]*/
domain_str : /(?: (?: [0-9a-z\-]+ | <addr> | <host> )+ \.
+? )+ /x
mac : /(?: [0-9a-f]{2} : ){5} [0-9a-f]{2} /ix
identifier : mac
{ ['hardware', lc $item[1]] }
| host_str
{ ['option host-name', $item[1]] }
ip : / (?: (?: 25[0-5]
| 2[0-4][0-9]
| [01]?[0-9]?[0-9] )\.
){3}
(?: 25[0-5]
| 2[0-4][0-9]
| [01]?[0-9]?[0-9]
) /x
{ $return = ip_32 ($item[1]); 1; }
name_str : /"[^"\n\t\r]+"/ { substr ($item[1], 1, -1) }
| /[^"\s]+/
duration : /(\d+)([smhdw]?)/ { $return = get_duration
+($thisline, $1, $2) }