#!/usr/bin/perl
use strict;
use warnings;
use YAML::XS;
use Data::Dumper;
my $data;
{local $/;$data = <>};
sub mergekeys
{
return _mergekeys($_[0], []);
}
sub _mergekeys
{
my $ref = shift;
my $resolveStack = shift;
my $reftype = ref $ref;
# If this hash or array is already on the resolution stack, then somewhere, a child data structure is trying to inherit from one of its parents,
# and hence by extension trying to inherit itself.
if ($reftype =~ /HASH|ARRAY/ and (grep $_ == $ref, @$resolveStack) > 0)
{
# Halt and catch fire, or store the cyclic reference and not process it further. Not complaining seems to
# be the behaviour of ruby's YAML parser, so let's go for that.
#die "Cyclic inheritance detected: ".($ref)." is already on the resolution stack!\n" .
# "Dump of cyclic data structure (may have inheritance already partially resolved):\n".Dumper($ref);
return $ref;
}
if (ref($ref) eq 'HASH')
{
push @$resolveStack, $ref;
if (exists $ref->{'<<'})
{
my $inherits = $ref->{'<<'}; # can be either a single href, or an array of hrefs
die "Undefined value for merge key '<<' in ".Dumper($ref) unless defined $inherits; # catch edge cases that YAML::XS won't catch, like "<<: &foo"
die "Merge key does not support merging non-hashmaps" unless ref($inherits) =~ /HASH|ARRAY/;
$inherits = [$inherits] if ref($inherits) eq 'HASH'; # normalize for further processing
# For each of the hashes/arrays we're inheriting, have them resolve their inheritance first before applying them onto ourselves.
# Also, remove the '<<' reference only afterwards, since by recursion these will have already been removed from our inheritees, # and this also allows us to show the cyclic reference by dumping out the structure when we detect one.
foreach my $inherit (@$inherits)
{
$inherit = _mergekeys($inherit, $resolveStack);
%$ref = (%$inherit, %$ref);
}
delete $ref->{'<<'};
}
_mergekeys($_, $resolveStack) for (values %$ref);
die "Fatal error: imbalanced recursion stack in _mergekeys. This likely implies a programming error and/or a YAML file from hell." unless pop(@$resolveStack) eq $ref;
}
elsif (ref($ref) eq 'ARRAY')
{
push @$resolveStack, $ref;
_mergekeys($_, $resolveStack) for (@$ref);
die "Fatal error: imbalanced recursion stack in _mergekeys. This likely implies a programming error and/or a YAML file from hell." unless pop(@$resolveStack) eq $ref;
}
return $ref;
}
my $yml = Load($data);
mergekeys $yml;
print Dumper $yml;
####
---
key0: &id00
zero: 0
key1: &id01
<<: *id00
subkey_a: asdf
subkey_b: qwer
Z: "key1.Z"
key2: &id02
everyday: apple
X: 1
Y: 2
Z: "key2.Z"
A:
- <<: *id00
zero: "00"
- <<: *id01
- <<: *id02 #cycle!
everyday: shuffling
Y: 3000
<<: *id00
key3: &id03
<<: [*id01, *id02, *id01]
subkey_a: foo
subkey_c: bar
deeper:
challenger: deep
<<: *id00
##
##
$ perl merge_keys.pl cyclic_test2.yml
$VAR1 = {
'key2' => {
'everyday' => 'apple',
'Z' => 'key2.Z',
'A' => [
{
'zero' => '00'
},
{
'Z' => 'key1.Z',
'subkey_b' => 'qwer',
'subkey_a' => 'asdf',
'zero' => 0
},
{
'A' => $VAR1->{'key2'}{'A'},
'Z' => 'key2.Z',
'everyday' => 'shuffling',
'X' => 1,
'zero' => 0,
'Y' => 3000
}
],
'X' => 1,
'zero' => 0,
'Y' => 2
},
'key1' => {
'subkey_b' => 'qwer',
'Z' => 'key1.Z',
'subkey_a' => 'asdf',
'zero' => 0
},
'key0' => {
'zero' => 0
},
'key3' => {
'A' => $VAR1->{'key2'}{'A'},
'subkey_b' => 'qwer',
'subkey_a' => 'foo',
'X' => 1,
'Y' => 2,
'everyday' => 'apple',
'Z' => 'key1.Z',
'subkey_c' => 'bar',
'zero' => 0,
'deeper' => {
'challenger' => 'deep',
'zero' => 0
}
}
};
##
##
$ ruby -ryaml -rpp -e 'x = YAML.load_file("cyclic_test2.yml"); print PP.pp(x);'
{"key0"=>{"zero"=>0},
"key1"=>{"zero"=>0, "subkey_a"=>"asdf", "subkey_b"=>"qwer", "Z"=>"key1.Z"},
"key2"=>
{"everyday"=>"apple",
"X"=>1,
"Y"=>2,
"Z"=>"key2.Z",
"A"=>
[{"zero"=>"00"},
{"zero"=>0, "subkey_a"=>"asdf", "subkey_b"=>"qwer", "Z"=>"key1.Z"},
{"everyday"=>"shuffling", "X"=>1, "Y"=>3000, "Z"=>"key2.Z"}],
"zero"=>0},
"key3"=>
{"zero"=>0,
"subkey_a"=>"foo",
"subkey_b"=>"qwer",
"Z"=>"key1.Z",
"everyday"=>"apple",
"X"=>1,
"Y"=>2,
"A"=>
[{"zero"=>"00"},
{"zero"=>0, "subkey_a"=>"asdf", "subkey_b"=>"qwer", "Z"=>"key1.Z"},
{"everyday"=>"shuffling", "X"=>1, "Y"=>3000, "Z"=>"key2.Z"}],
"subkey_c"=>"bar",
"deeper"=>{"challenger"=>"deep", "zero"=>0}}}