hotshot has asked for the wisdom of the Perl Monks concerning the following question:

hello dear monks!

I have two hashes in the following format:
my %hash = ( '' => 'nfs,1', '' => 'cifs,0', '' => 'afp,1', '' => 'nfs,0', '' => 'afp,0', );
As you can see, the key is an IP address and the value is a file protocol name and an index.
the first hash is old configuration, and the second hash will be new configuration. I need to restart the protocol(s) that one or more of their IP addresses were changed (including an index change), therefore I need some kind of diff function that will return me the changed protocols, for example afp and I'll know to restart it.
Anyone with an idea?


Replies are listed 'Best First'.
Re: Diff on hashes
by djantzen (Priest) on Nov 12, 2002 at 13:55 UTC

    There are several CPAN modules that I think will do what you need. Perhaps Data::Compare or Class::MakeMethods::Utility::Ref will do the trick. Long ago there was Ref, but I think that has gone away

    Update: Hmm, actually you need more than what those can do. Perhaps you could use one of the above to test whether the structure has changed, and if it has you could grep the keys of the two versions to see which keys are different. Of course that means keeping around two copies of the "same" hash -- one for control and the other for actual use -- but it sounds like you mean to do that anyway.

    Update 2: Check out Dominus's Tie::HashHistory which provides a kind of journalling hash.

Re: Diff on hashes
by robartes (Priest) on Nov 12, 2002 at 14:31 UTC
    I've recently done something similar in an environment where I can't use modules for various reasons, some of them even valid. Basically, you delete entries that are common to both hashes from both hashes. The hash with old entries will end up with the entries to delete, the one with new entries with the entries to add. A modification of an entry will become a delete/create.
    use strict; use Data::Dumper; my %oldhash=( '' => 'nfs,1', '' => 'cifs,0', '' => 'afp,1', '' => 'nfs,0', '' => 'afp,0', ); my %newhash=( '' => 'nfs,1', '' => 'cifs,0', '' => 'afp,2', '' => 'nfs,0', '' => 'afp,0', '' => 'cifs,1', ); foreach my $ip (keys %newhash) { if (exists($oldhash{$ip}) && $newhash{$ip} eq $oldhash{$ip}) { delete $oldhash{$ip}; delete $newhash{$ip}; } } print "Entries to remove:\n"; print Dumper(\%oldhash); print "Entries to add:\n"; print Dumper(\%newhash);
    Note that your current data structure is limited: if you have an IP that serves multiple protocols, you'll need to step up to a HoA or a HoH (and my code above will break).


Re: Diff on hashes
by Wonko the sane (Deacon) on Nov 12, 2002 at 14:22 UTC
    How about this?

    #!/usr/local/bin/perl -w use strict; use Data::Dumper; my %old_hash = ( '' => 'nfs,1', '' => 'cifs,0', '' => 'afp,1', '' => 'nfs,0', '' => 'afp,0', ); my %new_hash = ( '' => 'nfs,0', # index change '' => 'cifs,0', # IP change '' => 'afp,1', # IP change '' => 'nfs,1', # index change '' => 'afp,0', '' => 'afp,2', # new entry ); my %changes; foreach my $key ( keys %new_hash ) { $changes{$key} = $new_hash{$key} unless ( (exists $old_hash{$key}) && $new_hash{$key} eq $old_h +ash{$key} ); # gotta check for existence to quiet warnings. } print Dumper( \%changes );
    :!./ $VAR1 = { '' => 'afp,2', '' => 'nfs,0', '' => 'nfs,1', '' => 'cifs,0', '' => 'afp,1' };
    Let me know if you see anything that it would miss.
      Thanks for your answer, it's working almost perfectly. the thing is that it don't detect entries that were deleted from the old hash, do you have any idea how to change your solution so it'll handle that too?

Re: Diff on hashes
by reyjrar (Hermit) on Nov 12, 2002 at 14:24 UTC
    with perl, TIMTOWTDI :)

    my %current,%last; my %keys; map { $keys{$_}; } keys %current, keys %last; my %changes = (); foreach my $k (keys %keys) { $changes{$k} = $current{$k} unless $current{$k} eq $last{$k}; }
    I suppose you could get fancier than that, that'll give you a hash of ips => services if they vary from the previous. Creating the %keys hash allows you to add or subtract ips from your set without causing problems. When you go to act on the %changes hash, just check for an undef() value for that key, if its undef, then decide what to do. If you want to add the ability to stop "watching" an ip's service and leave it as is, you could do this:
    my %current,%last,%keys; foreach my $k (keys %current,keys %last) { next unless(exists $current{$k}); $keys{$k} = 1; }

    Of course, if you wanted to report the IPs that were removed, you could instead push them onto an array of @removed_ips or something like that. You could do the same for services using values() instead of keys().

    Sounds like an interesting project, hope you have fun with it.

Re: Diff on hashes
by rinceWind (Monsignor) on Nov 13, 2002 at 11:33 UTC
    Hi Hotshot,

    A few months ago, I posted Need a test for deep equality. My requirements were rather more exacting than yours, but the discussion is pertinent. As the order of keys in a hash is arbitrary, I discovered a lack of canonical hash ordering in Data::Dumper. This may have been fixed.

    In the end, the module that fever pointed out, the obscurely named Class::MakeMethods::Utility::Ref matched my requirement best. I did find an issue with code refs, as I was doing clever stuff with persisting and reconstituting them - I made a small change to the 'Ref' code to deparse coderefs and compare the deparse. However, I digress.

    What does your code need to know? Is it just a boolean condition - telling the server to restart, or do you want a breakdown of the differences? To get a list of differences, I would suggest transforming the hashes into canonicalized arrays, and using Array::Compare or Algorithm::Diff.

    Warning: untested code

    sub canonical_list (%) { my $in = shift; map {$_,$in->{$_}}, sort keys %$in; }