package Data::TreeDumper ; use 5.006 ; use strict ; use warnings ; use Carp ; require Exporter ; use AutoLoader qw(AUTOLOAD) ; our @ISA = qw(Exporter) ; our %EXPORT_TAGS = ('all' => [ qw() ]) ; our @EXPORT_OK = ( @{$EXPORT_TAGS{'all'} } ) ; our @EXPORT = qw(DumpTree PrintTree DumpTrees CreateChainingFilter); our $VERSION = '0.33' ; my $WIN32_CONSOLE ; BEGIN { if($^O ne 'MSWin32') { eval "use Term::Size;" ; die $@ if $@ ; } else { eval "use Win32::Console;" ; die $@ if $@ ; $WIN32_CONSOLE= new Win32::Console; } } use Text::Wrap ; use Class::ISA ; use Sort::Naturally ; #------------------------------------------------------------------------------- # setup values #------------------------------------------------------------------------------- our %setup = ( FILTER => undef , FILTER_ARGUMENT => undef , LEVEL_FILTERS => undef , TYPE_FILTERS => undef , USE_ASCII => 1 , MAX_DEPTH => -1 , INDENTATION => '' , NO_OUTPUT => 0 , START_LEVEL => 1 , VIRTUAL_WIDTH => 120 , DISPLAY_ROOT_ADDRESS => 0 , DISPLAY_ADDRESS => 1 , DISPLAY_PATH => 0 , DISPLAY_OBJECT_TYPE => 1 , DISPLAY_INHERITANCE => 0 , DISPLAY_TIE => 0 , DISPLAY_AUTOLOAD => 0 , DISPLAY_PERL_SIZE => 0 , DISPLAY_PERL_ADDRESS => 0 , NUMBER_LEVELS => 0 , COLOR_LEVELS => undef , GLYPHS => ['| ', '|- ', '`- ', ' '] , QUOTE_HASH_KEYS => 0 , QUOTE_VALUES => 0 , REPLACEMENT_LIST => [ ["\n" => '[\n]'], ["\r" => '[\r]'], ["\t" => '[\t]'] ] , DISPLAY_NUMBER_OF_ELEMENTS_OVER_MAX_DEPTH => 0 , DISPLAY_CALLER_LOCATION=> 0 , __DATA_PATH => '' , __PATH_ELEMENTS => [] , __TYPE_SEPARATORS => { '' => [''] , 'REF' => ['<', '>'] , 'CODE' => [''] , 'HASH' => ['{\'', '\'}'] , 'ARRAY' => ['[', ']'] , 'SCALAR' => [''] } ) ; #---------------------------------------------------------------------- # package variables à la Data::Dumper (as is the silly naming scheme) #---------------------------------------------------------------------- our $Filter = $setup{FILTER} ; our $Filterarguments = $setup{FILTER_ARGUMENT} ; our $Levelfilters = $setup{LEVEL_FILTERS} ; our $Typefilters = $setup{TYPE_FILTERS} ; our $Useascii = $setup{USE_ASCII} ; our $Maxdepth = $setup{MAX_DEPTH} ; our $Indentation = $setup{INDENTATION} ; our $Nooutput = $setup{NO_OUTPUT} ; our $Startlevel = $setup{START_LEVEL} ; our $Virtualwidth = $setup{VIRTUAL_WIDTH} ; our $Displayrootaddress = $setup{DISPLAY_ROOT_ADDRESS} ; our $Displayaddress = $setup{DISPLAY_ADDRESS} ; our $Displaypath = $setup{DISPLAY_PATH} ; our $Displayobjecttype = $setup{DISPLAY_OBJECT_TYPE} ; our $Displayinheritance = $setup{DISPLAY_INHERITANCE} ; our $Displaytie = $setup{DISPLAY_TIE} ; our $Displayautoload = $setup{DISPLAY_AUTOLOAD} ; our $Displayperlsize = $setup{DISPLAY_PERL_SIZE} ; our $Displayperladdress = $setup{DISPLAY_PERL_ADDRESS} ; our $Numberlevels = $setup{NUMBER_LEVELS} ; our $Colorlevels = $setup{COLOR_LEVELS} ; our $Glyphs = [@{$setup{GLYPHS}}] ; # we don't want it to be shared our $Quotehashkeys = $setup{QUOTE_HASH} ; our $Quotevalues = $setup{QUOTE_VALUES} ; our $ReplacementList = [@{$setup{REPLACEMENT_LIST}}] ; # we don't want it to be shared our $Displaynumberofelementsovermaxdepth = $setup{DISPLAY_NUMBER_OF_ELEMENTS_OVER_MAX_DEPTH} ; our $Displaycallerlocation= $setup{DISPLAY_CALLER_LOCATION} ; #~ our $Deparse = 0 ; # not implemented sub GetPackageSetup { return ( FILTER => $Data::TreeDumper::Filter , FILTER_ARGUMENT => $Data::TreeDumper::Filterarguments , LEVEL_FILTERS => $Data::TreeDumper::Levelfilters , TYPE_FILTERS => $Data::TreeDumper::Typefilters , USE_ASCII => $Data::TreeDumper::Useascii , MAX_DEPTH => $Data::TreeDumper::Maxdepth , INDENTATION => $Data::TreeDumper::Indentation , NO_OUTPUT => $Data::TreeDumper::Nooutput , START_LEVEL => $Data::TreeDumper::Startlevel , VIRTUAL_WIDTH => $Data::TreeDumper::Virtualwidth , DISPLAY_ROOT_ADDRESS => $Data::TreeDumper::Displayrootaddress , DISPLAY_ADDRESS => $Data::TreeDumper::Displayaddress , DISPLAY_PATH => $Data::TreeDumper::Displaypath , DISPLAY_OBJECT_TYPE => $Data::TreeDumper::Displayobjecttype , DISPLAY_INHERITANCE => $Data::TreeDumper::Displayinheritance , DISPLAY_TIE => $Data::TreeDumper::Displaytie , DISPLAY_AUTOLOAD => $Data::TreeDumper::Displayautoload , DISPLAY_PERL_SIZE => $Data::TreeDumper::Displayperlsize , DISPLAY_PERL_ADDRESS => $Data::TreeDumper::Displayperladdress , NUMBER_LEVELS => $Data::TreeDumper::Numberlevels , COLOR_LEVELS => $Data::TreeDumper::Colorlevels , GLYPHS => $Data::TreeDumper::Glyphs , QUOTE_HASH_KEYS => $Data::TreeDumper::Quotehashkeys , REPLACEMENT_LIST => $Data::TreeDumper::ReplacementList , DISPLAY_NUMBER_OF_ELEMENTS_OVER_MAX_DEPTH => $Displaynumberofelementsovermaxdepth , DISPLAY_CALLER_LOCATION=> $Displaycallerlocation , __DATA_PATH => '' , __PATH_ELEMENTS => [] , __TYPE_SEPARATORS => $setup{__TYPE_SEPARATORS} ) ; } #------------------------------------------------------------------------------- # API #------------------------------------------------------------------------------- sub PrintTree { my ($package, $file_name, $line) = caller() ; print DumpTree(@_, DUMPER_NAME => "PrintTree at '$file_name:$line'") ; } sub DumpTree { my $structure_to_dump = shift ; my $title = shift ; my %overrides = @_ ; $title = defined $title ? $title : '' ; my ($package, $file_name, $line) = caller() ; my $location = '' ; if($Displaycallerlocation) { $location = defined $overrides{DUMPER_NAME} ? $overrides{DUMPER_NAME} : "DumpTree at '$file_name:$line'" ; } unless(defined $structure_to_dump) { return("$title (undefined variable) $location\n") ; } if('' eq ref $structure_to_dump) { return("$title $structure_to_dump (scalar variable) $location\n"); } if($Displaycallerlocation) { print "$location\n" ; } my %local_setup ; if(exists $overrides{NO_PACKAGE_SETUP} && $overrides{NO_PACKAGE_SETUP}) { %local_setup = (%setup, %overrides) ; } else { %local_setup = (GetPackageSetup(), %overrides) ; } unless (exists $local_setup{TYPE_FILTERS}{Regexp}) { # regexp objecjts (created with qr) are dumped by the below sub $local_setup{TYPE_FILTERS}{Regexp} = sub { my ($regexp) = @_ ; return ('HASH', {REGEXP=> "$regexp"}, 'REGEXP') ; } ; } return(TreeDumper($structure_to_dump, {TITLE => $title, %local_setup})) ; } #------------------------------------------------------------------------------- sub DumpTrees { my @trees = grep {'ARRAY' eq ref $_} @_ ; my %global_overrides = grep {'ARRAY' ne ref $_} @_ ; my $dump = '' ; for my $tree (@trees) { my ($structure_to_dump, $title, %overrides) = @{$tree} ; $title = defined $title ? $title : '' ; if(defined $structure_to_dump) { $dump .= DumpTree($structure_to_dump, $title, %global_overrides, %overrides) ; } else { my ($package, $file_name, $line) = caller() ; $dump .= "DumpTrees can't dump 'undef' with title: '$title' @ '$file_name:$line'.\n" ; } } return($dump) ; } #------------------------------------------------------------------------------- # The dumper #------------------------------------------------------------------------------- sub TreeDumper { my $tree = shift ; my $setup = shift ; my $level = shift || 0 ; my $levels_left = shift || [] ; my $tree_type = ref $tree ; confess "TreeDumper can only display objects passed by reference!\n" if('' eq $tree_type) ; my $already_displayed_nodes = shift || {$tree => GetReferenceType($tree) . 'O', NEXT_INDEX => 1} ; return('') if ($setup->{MAX_DEPTH} == $level) ; #-------------------------- # perl data size #-------------------------- if($level == 0) { eval 'use Devel::Size qw(size total_size) ;' ; if($@) { # shoud we warn ??? delete $setup->{DISPLAY_PERL_SIZE} ; } } local $Devel::Size::warn = 0 if($level == 0) ; #-------------------------- # filters #-------------------------- my ($replacement_tree, $nodes_to_display) ; my ($filter_sub, $filter_argument) = GetFilter($setup, $level, ref $tree) ; if(defined $filter_sub) { ($tree_type, $replacement_tree, @$nodes_to_display) = $filter_sub->($tree, $level, $setup->{__DATA_PATH}, $nodes_to_display, $setup, $filter_argument) ; $tree = $replacement_tree if(defined $replacement_tree) ; } else { ($tree_type, undef, @$nodes_to_display) = DefaultNodesToDisplay($tree) ; } return('') unless defined $tree_type ; #easiest way to prune in a filter is to return undef as type # filters can change the name of the nodes by passing an array ref my @node_names ; my @nodes_to_display = @$nodes_to_display ; for my $node (@nodes_to_display) { if('ARRAY' eq ref $node) { push @node_names, $node->[1] ; $node = $node->[0] ; # Modify $nodes_to_display } else { push @node_names, $node ; } } #-------------------------- # dump #-------------------------- my $output = '' ; $output .= RenderRoot($tree, $setup) if($level == 0) ; my ($opening_bracket, $closing_bracket) = GetBrackets($setup, $tree_type) ; for (my $node_index = 0 ; $node_index < @nodes_to_display ; $node_index++) { my $nodes_left = (@nodes_to_display - 1) - $node_index ; $levels_left->[$level] = $nodes_left ; my @separator_data = GetSeparator ( $level , $nodes_left , $levels_left , $setup->{START_LEVEL} , $setup->{GLYPHS} , $setup->{COLOR_LEVELS} ) ; my ($element, $element_name, $element_address, $element_id) = GetElement($tree, $tree_type, \@nodes_to_display, \@node_names, $node_index, $setup); my $is_terminal_node = IsTerminalNode ( $element , $element_name , $level , $setup ) ; if(! $is_terminal_node && exists $already_displayed_nodes->{$element_address}) { $is_terminal_node = 1 ; } my $element_name_rendering = RenderElementName ( \@separator_data , $tree, $tree_type, \@nodes_to_display, \@node_names, $node_index , $level , $levels_left , $already_displayed_nodes , $setup ) ; unless($is_terminal_node) { local $setup->{__DATA_PATH} = "$setup->{__DATA_PATH}$opening_bracket$element_name$closing_bracket" ; push @{$setup->{__PATH_ELEMENTS}}, [$tree_type, $element_name, $tree] ; my $sub_tree_dump = TreeDumper($element, $setup, $level + 1, $levels_left, $already_displayed_nodes) ; $output .= $element_name_rendering .$sub_tree_dump ; pop @{$setup->{__PATH_ELEMENTS}} ; } else { $output .= $element_name_rendering ; } } RenderEnd(\$output, $setup) if($level == 0) ; return($output) ; } #------------------------------------------------------------------------------- sub GetFilter { my ($setup, $level, $type) = @_ ; my $filter_sub = $setup->{FILTER} ; # specific level filter has higher priority my $level_filters = $setup->{LEVEL_FILTERS} ; $filter_sub = $level_filters->{$level} if(defined $level_filters && exists $level_filters->{$level}) ; my $type_filters = $setup->{TYPE_FILTERS} ; $filter_sub = $type_filters->{$type} if(defined $type_filters && exists $type_filters->{$type}) ; unless ('CODE' eq ref $filter_sub || ! defined $filter_sub) { my ($package, $file_name, $line) = caller(2) ; die "DumpTree: FILTER must be sub reference at '$file_name:$line'" ; } return($filter_sub, $setup->{FILTER_ARGUMENT}) ; } #------------------------------------------------------------------------------- sub GetElement { my ($tree, $tree_type, $nodes_to_display, $node_names, $node_index, $setup) = @_ ; my ($element, $element_name, $element_address, $element_id) ; for($tree_type) { # TODO, move this out of the loop with static table of functions 'HASH' eq $_ and do { $element = $tree->{$nodes_to_display->[$node_index]} ; $element_address = "$element" if defined $element ; if($setup->{QUOTE_HASH_KEYS}) { $element_name = "'$node_names->[$node_index]'" ; } else { $element_name = $node_names->[$node_index] ; } $element_id = \($tree->{$nodes_to_display->[$node_index]}) ; last } ; 'ARRAY' eq $_ and do { #~ # debug while writting Diff module #~ unless(defined $nodes_to_display->[$node_index]) #~ { #~ use Data::Dumper ; #~ print Dumper $nodes_to_display ; #~ my ($package, $file_name, $line) = caller() ; #~ print "Called from $file_name, $line\n" ; #~ print "$tree->\[$nodes_to_display->\[$node_index\]\]\n" ; #~ } $element = $tree->[$nodes_to_display->[$node_index]] ; $element_address = "$element" if defined $element ; $element_name = $node_names->[$node_index] ; $element_id = \($tree->[$nodes_to_display->[$node_index]]) ; last ; } ; 'REF' eq $_ and do { $element = $$tree ; $element_address = "$element" if defined $element ; $element_name = "$tree" ; $element_id = $tree ; last ; } ; 'CODE' eq $_ and do { $element = $tree ; $element_address = "$element" if defined $element ; $element_name = $tree ; $element_id = $tree ; last ; } ; ('SCALAR' eq $_) and do #~ ('SCALAR' eq $_ or 'GLOB' eq $_) and do { $element = $$tree ; $element_address = "$element" if defined $element ; $element_name = '?' ; $element_id = $tree ; last ; } ; } return ($element, $element_name, $element_address, $element_id) ; } #---------------------------------------------------------------------- sub RenderElementName { my ( $separator_data , $tree, $tree_type, $nodes_to_display, $node_names, $node_index , $level , $levels_left , $already_displayed_nodes , $setup ) = @_ ; return('') unless defined $tree ; my ($opening_bracket, $closing_bracket) = GetBrackets($setup, $tree_type) ; my ($element, $element_name, $element_address, $element_id) = GetElement($tree, $tree_type, $nodes_to_display, $node_names, $node_index, $setup); my @rendering_elements = GetElementInfo ( $element , $element_name , $element_address , $element_id , $level , $already_displayed_nodes , $setup ) ; my $output = RenderNode ( $element , $element_name , $level , @$separator_data , @rendering_elements , $setup ) ; return($output) ; } #------------------------------------------------------------------------------- sub GetBrackets { my ($setup, $tree_type) = @_ ; my ($opening_bracket, $closing_bracket) ; if(exists $setup->{__TYPE_SEPARATORS}{$tree_type}) { ($opening_bracket, $closing_bracket) = @{$setup->{__TYPE_SEPARATORS}{$tree_type}} ; } else { ($opening_bracket, $closing_bracket) = ('') ; } return($opening_bracket, $closing_bracket) ; } #------------------------------------------------------------------------------- sub RenderEnd { my ($output_ref, $setup) = @_ ; return('') if $setup->{NO_OUTPUT} ; if(defined $setup->{RENDERER}{END}) { $$output_ref .= $setup->{RENDERER}{END}($setup) ; } else { unless ($setup->{USE_ASCII}) { # convert to ANSI $$output_ref =~ s/\| /\033(0\170 \033(B/g ; $$output_ref =~ s/\|- /\033(0\164\161 \033(B/g ; $$output_ref =~ s/\`- /\033(0\155\161 \033(B/g ; } } } #------------------------------------------------------------------------------- sub RenderRoot { my ($tree, $setup) = @_ ; my $output = '' ; if(defined $setup->{RENDERER} && '' eq ref $setup->{RENDERER}) { eval <{RENDERER} ; \$setup->{RENDERER} = Data::TreeDumper::Renderer::$setup->{RENDERER}::GetRenderer() ; EOE die "Data::TreeDumper couldn't load renderer '$setup->{RENDERER}':\n$@" if $@ ; } if(defined $setup->{RENDERER}{NAME}) { eval <{RENDERER}{NAME} ; \$setup->{RENDERER} = {%{\$setup->{RENDERER}}, %{Data::TreeDumper::Renderer::$setup->{RENDERER}{NAME}::GetRenderer()}} ; EOE die "Data::TreeDumper couldn't load renderer '$setup->{RENDERER}{NAME}':\n$@" if $@ ; } unless($setup->{NO_OUTPUT}) { my $root_tie_and_class = GetElementTieAndClass($setup, $tree) ; if(defined $setup->{RENDERER}{BEGIN}) { my $root_address = '' ; $root_address = GetReferenceType($tree) . 'O' if($setup->{DISPLAY_ROOT_ADDRESS}) ; my $perl_address = '' ; $perl_address = $tree if($setup->{DISPLAY_PERL_ADDRESS}) ; my $perl_size = '' ; $perl_size = total_size($tree) if($setup->{DISPLAY_PERL_SIZE}) ; $output .= $setup->{RENDERER}{BEGIN}($setup->{TITLE} . $root_tie_and_class, $root_address, $tree, $perl_size, $perl_address, $setup) ; } else { $output .= $setup->{INDENTATION} ; $output .= defined $setup->{TITLE} ? $setup->{TITLE} : '' ; $output .= $root_tie_and_class ; $output .= ' [' . GetReferenceType($tree) . "0]" if($setup->{DISPLAY_ROOT_ADDRESS}) ; $output .= " $tree" if($setup->{DISPLAY_PERL_ADDRESS}) ; $output .= " <" . total_size($tree) . ">" if($setup->{DISPLAY_PERL_SIZE}) ; $output .= "\n" ; } } return($output) ; } #------------------------------------------------------------------------------- sub RenderNode { my ( $element , $element_name , $level , $previous_level_separator , $separator , $subsequent_separator , $separator_size , $is_terminal_node , $perl_size , $perl_address , $tag , $element_value , $default_element_rendering , $dtd_address , $address_field , $address_link , $setup ) = @_ ; my $output = '' ; return('') if $setup->{NO_OUTPUT} ; if(defined $setup->{RENDERER}{NODE}) { #~ #TODO: some elements are not available in this function, pass them from caller $output .= $setup->{RENDERER}{NODE} ( $element , $level , $is_terminal_node , $previous_level_separator , $separator , $element_name , $element_value , $dtd_address , $address_link , $perl_size , $perl_address , $setup ) ; } else { #-------------------------- # wrapping #-------------------------- my $level_text = GetLevelText($element, $level, $setup) ; my $tree_header = $setup->{INDENTATION} . $level_text . $previous_level_separator . $separator ; my $tree_subsequent_header = $setup->{INDENTATION} . $level_text . $previous_level_separator . $subsequent_separator ; my $element_description = $element_name . $default_element_rendering ; $perl_size = " <$perl_size> " unless $perl_size eq '' ; $element_description .= " $address_field$perl_size$perl_address\n" ; if($setup->{NO_WRAP}) { $output .= $tree_header ; $output .= $element_description ; } else { my ($columns, $rows) ; if($^O ne 'MSWin32') { eval "(\$columns, \$rows) = Term::Size::chars *STDOUT{IO} ;" ; } else { ($columns, $rows) = $WIN32_CONSOLE->Size(); } if($columns eq '') { $columns = $setup->{VIRTUAL_WIDTH} ; } else { $columns = $setup->{WRAP_WIDTH} if defined $setup->{WRAP_WIDTH} ; } local $Text::Wrap::columns = $columns ; local $Text::Wrap::unexpand = 0 ; if(length($tree_header) + length($element_description) > $columns && ! $setup->{NO_WRAP}) { $output .= wrap ( $tree_header , $tree_subsequent_header , $element_description ) ; } else { $output .= $tree_header ; $output .= $element_description ; } } } return($output) ; } #------------------------------------------------------------------------------- sub GetElementInfo { my ( $element , $element_name , $element_address , $element_id , $level , $already_displayed_nodes , $setup ) = @_ ; my $perl_size = '' ; $perl_size = total_size($element) if($setup->{DISPLAY_PERL_SIZE}) ; my $perl_address = "" ; my $tag = '' ; my $element_value = '' ; my $is_terminal_node = 0 ; my $default_element_rendering = '' ; for(ref $element) { '' eq $_ and do { $is_terminal_node++ ; $tag = 'S' ; $element_address = $already_displayed_nodes->{NEXT_INDEX} ; my $value = defined $element ? $element : 'undef' ; $element_value = "$value" ; my $replacement_list = $setup->{REPLACEMENT_LIST} ; if(defined $replacement_list) { for my $replacement (@$replacement_list) { my $find = $replacement->[0] ; my $replace = $replacement->[1] ; $element_value =~ s/$find/$replace/g ; } } if($setup->{QUOTE_VALUES} && defined $element) { $default_element_rendering = " = '$element_value'" ; } else { $default_element_rendering = " = $element_value" ; } $perl_address = "$element_id" if($setup->{DISPLAY_PERL_ADDRESS}) ; # $setup->{DISPLAY_TIE} doesn't make sense as scalars are copied last ; } ; 'HASH' eq $_ and do { $is_terminal_node = IsTerminalNode ( $element , $element_name , $level , $setup ) ; $tag = 'H' ; $perl_address = "$element" if($setup->{DISPLAY_PERL_ADDRESS}) ; if(! %{$element} && ! $setup->{NO_NO_ELEMENTS}) { $default_element_rendering = $element_value = ' (no elements)' ; } if ( %{$element} && ( (($setup->{MAX_DEPTH} == $level + 1) && $setup->{DISPLAY_NUMBER_OF_ELEMENTS_OVER_MAX_DEPTH}) || $setup->{DISPLAY_NUMBER_OF_ELEMENTS} ) ) { my $number_of_elements = keys %{$element} ; my $plural = $number_of_elements > 1 ? 's' : '' ; my $elements = ' (' . $number_of_elements . ' element' . $plural . ')' ; $default_element_rendering .= $elements ; $element_value .= $elements ; } if($setup->{DISPLAY_TIE} && (my $tie = tied %$element)) { $tie =~ s/=.*$// ; my $tie = " (tied to '$tie')" ; $default_element_rendering .= $tie ; $element_value .= $tie ; } last ; } ; 'ARRAY' eq $_ and do { $is_terminal_node = IsTerminalNode ( $element , $element_name , $level , $setup ) ; $tag = 'A' ; $perl_address = "$element" if($setup->{DISPLAY_PERL_ADDRESS}) ; if(! @{$element} && ! $setup->{NO_NO_ELEMENTS}) { $default_element_rendering = $element_value .= ' (no elements)' ; } if ( @{$element} && ( (($setup->{MAX_DEPTH} == $level + 1) && $setup->{DISPLAY_NUMBER_OF_ELEMENTS_OVER_MAX_DEPTH}) || $setup->{DISPLAY_NUMBER_OF_ELEMENTS} ) ) { my $plural = scalar(@{$element}) ? 's' : '' ; my $elements = ' (' . @{$element} . ' element' . $plural . ')' ; $default_element_rendering .= $elements ; $element_value .= $elements ; } if($setup->{DISPLAY_TIE} && (my $tie = tied @$element)) { $tie =~ s/=.*$// ; my $tie = " (tied to '$tie')" ; $default_element_rendering .= $tie ; $element_value .= $tie ; } last ; } ; 'CODE' eq $_ and do { $is_terminal_node++ ; $tag = 'C' ; #~ use Data::Dump::Streamer; #~ $element_value = "----- " . Dump($element)->Out() ; $element_value = "$element" ; $default_element_rendering= " = $element_value" ; $perl_address = "$element_id" if($setup->{DISPLAY_PERL_ADDRESS}) ; last ; } ; 'SCALAR' eq $_ and do { $is_terminal_node = 0 ; $tag = 'RS' ; $element_address = $element_id ; $perl_address = "$element_id" if($setup->{DISPLAY_PERL_ADDRESS}) ; last ; } ; 'GLOB' eq $_ and do { $is_terminal_node++ ; $tag = 'G' ; $perl_address = "$element" if($setup->{DISPLAY_PERL_ADDRESS}) ; last ; } ; 'REF' eq $_ and do { $is_terminal_node = 0 ; $tag = 'R' ; $perl_address = "$element" if($setup->{DISPLAY_PERL_ADDRESS}) ; last ; } ; # DEFAULT, an object. $tag = 'O' ; if($element =~ /=HASH/ ) { $tag = 'OH' ; } elsif($element =~ /=ARRAY/) { $tag = 'OA' ; } elsif($element =~ /=GLOB/) { $tag = 'OG' ; } elsif($element =~ /=SCALAR/) { $tag = 'OS' ; } $perl_address = "$element" if($setup->{DISPLAY_PERL_ADDRESS}) ; #check if the object is empty and display that state if NO_NO_ELEMENT isn't set ($is_terminal_node, my $element_value) = IsTerminalNode ( $element , $element_name , $level , $setup ) ; if($setup->{DISPLAY_OBJECT_TYPE}) { $element_value .= GetElementTieAndClass($setup, $element) ; $default_element_rendering = " = $element_value" ; } } # address my $dtd_address = $tag . $already_displayed_nodes->{NEXT_INDEX} ; my $address_field = '' ; my $address_link ; if(exists $already_displayed_nodes->{$element_address}) { $already_displayed_nodes->{NEXT_INDEX}++ ; $address_field = " [$dtd_address -> $already_displayed_nodes->{$element_address}]" if $setup->{DISPLAY_ADDRESS} ; $address_link = $already_displayed_nodes->{$element_address} ; $is_terminal_node = 1 ; } else { $already_displayed_nodes->{$element_address} = $dtd_address ; $already_displayed_nodes->{$element_address} .= " /$setup->{__DATA_PATH}" if $setup->{DISPLAY_PATH}; $already_displayed_nodes->{NEXT_INDEX}++ ; $address_field = " [$dtd_address]" if $setup->{DISPLAY_ADDRESS} ; } return ( $is_terminal_node , $perl_size , $perl_address , $tag , $element_value , $default_element_rendering , $dtd_address , $address_field , $address_link ) ; } #---------------------------------------------------------------------- sub IsTerminalNode { my ( $element , $element_name , $level , $setup ) = @_ ; my $is_terminal_node = 0 ; my $element_value = '' ; my ($filter_sub, $filter_argument) = GetFilter($setup, $level, ref $element) ; for(ref $element) { '' eq $_ and do { $is_terminal_node = 1 ; last ; } ; 'HASH' eq $_ and do { # node is terminal if it has no children $is_terminal_node++ unless %$element ; # node might be terminal if filter says it has no children if(!$is_terminal_node && defined $setup->{RENDERER}{NODE}) { if(defined $filter_sub) { my @children_nodes_to_display ; local $setup->{__DATA_PATH} = "$setup->{__DATA_PATH}\{$element_name\}" ; (undef, undef, @children_nodes_to_display) = $filter_sub->($element, $level + 1, $setup->{__DATA_PATH}, \@children_nodes_to_display, $setup, $filter_argument) ; $is_terminal_node++ unless @children_nodes_to_display ; } } last ; } ; 'ARRAY' eq $_ and do { # node is terminal if it has no children $is_terminal_node++ unless(@$element) ; # node might be terminal if filter says it has no children if(!$is_terminal_node && defined $setup->{RENDERER}{NODE}) { if(defined $filter_sub) { my @children_nodes_to_display ; local $setup->{__DATA_PATH} = "$setup->{__DATA_PATH}\[$element_name\]" ; (undef, undef, @children_nodes_to_display) = $filter_sub->($element, $level + 1, $setup->{__DATA_PATH}, \@children_nodes_to_display, $setup, $filter_argument) ; $is_terminal_node++ unless @children_nodes_to_display ; } } last ; } ; 'CODE' eq $_ and do { $is_terminal_node = 1 ; last ; } ; 'SCALAR' eq $_ and do { $is_terminal_node = 0 ; last ; } ; 'GLOB' eq $_ and do { $is_terminal_node = 1 ; last ; } ; 'REF' eq $_ and do { $is_terminal_node = 0 ; last ; } ; # DEFAULT, an object. #check if the object is empty and display that state if NO_NO_ELEMENT isn't set for($element) { /=HASH/ and do { unless(%$element) { $is_terminal_node++ ; unless($setup->{NO_NO_ELEMENTS}) { $element_value = "(Hash, empty) $element_value" ; } } last ; } ; /=ARRAY/ and do { unless(@$element) { $is_terminal_node++ ; unless($setup->{NO_NO_ELEMENTS}) { $element_value = "(Array, empty) $element_value" ; } } last ; } ; } } return($is_terminal_node, $element_value) if wantarray ; return($is_terminal_node) ; } #---------------------------------------------------------------------- sub GetElementTieAndClass { my ($setup, $element) = @_ ; my $element_type = '' ; if($setup->{DISPLAY_TIE}) { if("$element" =~ '=HASH' && (my $tie_hash = tied %$element)) { $tie_hash =~ s/=.*$// ; $element_type .= " (tied to '$tie_hash' [H])" } elsif("$element" =~ '=ARRAY' && (my $tie_array = tied @$element)) { $tie_array =~ s/=.*$// ; $element_type .= " (tied to '$tie_array' [A])" } elsif("$element" =~ '=SCALAR' && (my $tie_scalar = tied $$element)) { $tie_scalar =~ s/=.*$// ; $element_type .= " (tied to '$tie_scalar' [RS])" } elsif("$element" =~ '=GLOB' && (my $tie_glob = tied *$element)) { $tie_glob =~ s/=.*$// ; $element_type .= " (tied to '$tie_glob' [G])" } } for(ref $element) { '' eq $_ || 'HASH' eq $_ || 'ARRAY' eq $_ || 'CODE' eq $_ || 'SCALAR' eq $_ || 'GLOB' eq $_ || 'REF' eq $_ and do { last ; } ; # an object. if($setup->{DISPLAY_OBJECT_TYPE}) { my $class = ref($element) ; my $has_autoload ; eval "\$has_autoload = *${class}::AUTOLOAD{CODE} ;" ; $has_autoload = $has_autoload ? '[AL]' : '' ; $element_type .= " blessed in '$has_autoload$class'" ; if($setup->{DISPLAY_INHERITANCE}) { for my $base_class (Class::ISA::super_path(ref($element))) { if($setup->{DISPLAY_AUTOLOAD}) { no warnings ; eval "\$has_autoload = *${base_class}::AUTOLOAD{CODE} ;" ; if($has_autoload) { $element_type .= " <- [AL]$base_class " ; } else { $element_type .= " <- $base_class " ; } } else { $element_type .= " <- $base_class " ; } } } } } return($element_type) ; } #---------------------------------------------------------------------- # filters #---------------------------------------------------------------------- sub DefaultNodesToDisplay { my ($tree, undef, undef, $keys) = @_ ; return('', undef) if '' eq ref $tree ; my $tree_type = ref $tree ; if('HASH' eq $tree_type) { return('HASH', undef, @$keys) if(defined $keys) ; return('HASH', undef, nsort keys %$tree) ; } if('ARRAY' eq $tree_type) { return('ARRAY', undef, @$keys) if(defined $keys) ; return('ARRAY', undef, (0 .. @$tree - 1)) ; } return('SCALAR', undef, (0)) if('SCALAR' eq $tree_type) ; return('REF', undef, (0)) if('REF' eq $tree_type) ; return('CODE', undef, (0)) if('CODE' eq $tree_type) ; my @nodes_to_display ; undef $tree_type ; if($tree =~ /=/) { for($tree) { /=HASH/ and do { @nodes_to_display = nsort keys %$tree ; $tree_type = 'HASH' ; last ; } ; /=ARRAY/ and do { @nodes_to_display = (0 .. @$tree - 1) ; $tree_type = 'ARRAY' ; last ; } ; /=GLOB/ and do { @nodes_to_display = (0) ; $tree_type = 'REF' ; last ; } ; /=SCALAR/ and do { @nodes_to_display = (0) ; $tree_type = 'REF' ; last ; } ; warn "TreeDumper: Unsupported underlying type for $tree.\n" ; } } return($tree_type, undef, @nodes_to_display) ; } #------------------------------------------------------------------------------- sub HashKeysSorter { my ($structure_to_dump, undef, undef, $keys) = @_ ; if('HASH' eq ref $structure_to_dump) { return('HASH', undef, nsort keys %$structure_to_dump) unless defined $keys ; my %keys ; for my $key (@$keys) { if('ARRAY' eq ref $key) { $keys{$key->[0]} = $key ; } else { $keys{$key} = $key ; } } return('HASH', undef, map{$keys{$_}} nsort keys %keys) ; } return(Data::TreeDumper::DefaultNodesToDisplay($structure_to_dump)) ; } #---------------------------------------------------------------------- sub CreateChainingFilter { my @filters = @_ ; return sub { my $tree = shift ; my $level = shift ; my $path = shift ; my $keys = shift ; my ($tree_type, $replacement_tree); for my $filter (@filters) { ($tree_type, $replacement_tree, @$keys) = $filter->($tree, $level, $path, $keys) ; $tree = $replacement_tree if (defined $replacement_tree) ; } return ($tree_type, $replacement_tree, @$keys) ; } } ; #------------------------------------------------------------------------------- # rendering support #------------------------------------------------------------------------------- { # make %types private my %types = ( '' => 'SCALAR! not a reference!' , 'REF' => 'R' , 'CODE' => 'C' , 'HASH' => 'H' , 'ARRAY' => 'A' , 'SCALAR' => 'RS' ) ; sub GetReferenceType { my $element = shift ; my $reference = ref $element ; if(exists $types{$reference}) { return($types{$reference}) ; } else { my $tag = 'O' ; if($element =~ /=HASH/ ) { $tag = 'OH' ; } elsif($element =~ /=ARRAY/) { $tag = 'OA' ; } elsif($element =~ /=GLOB/) { $tag = 'OG' ; } elsif($element =~ /=SCALAR/) { $tag = 'OS' ; } return($tag) ; } } } # make %types private #------------------------------------------------------------------------------- sub GetLevelText { my ($element, $level, $setup) = @_ ; my $level_text = '' ; if($setup->{NUMBER_LEVELS}) { if('CODE' eq ref $setup->{NUMBER_LEVELS}) { $level_text = $setup->{NUMBER_LEVELS}->($element, $level, $setup) ; } else { my $color_levels = $setup->{COLOR_LEVELS} ; my ($color_start, $color_end) = ('', '') ; if($color_levels) { if('ARRAY' eq ref $color_levels) { my $color_index = $level % @{$color_levels->[0]} ; ($color_start, $color_end) = ($color_levels->[0][$color_index] , $color_levels->[1]) ; } else { # assume code ($color_start, $color_end) = $color_levels->($level) ; } } $level_text = sprintf("$color_start%$setup->{NUMBER_LEVELS}d$color_end ", ($level + 1)) ; } } return($level_text) ; } #---------------------------------------------------------------------- sub GetSeparator { my ( $level , $is_last_in_level , $levels_left , $start_level , $glyphs , $colors # array or code ref ) = @_ ; my $separator_size = 0 ; my $previous_level_separator = '' ; my ($color_start, $color_end) = ('', '') ; for my $current_level ((1 - $start_level) .. ($level - 1)) { $separator_size += 3 ; if($colors) { if('ARRAY' eq ref $colors) { my $color_index = $current_level % @{$colors->[0]} ; ($color_start, $color_end) = ($colors->[0][$color_index] , $colors->[1]) ; } else { if('CODE' eq ref $colors) { ($color_start, $color_end) = $colors->($current_level) ; } #else # ignore other types } } if(! defined $levels_left->[$current_level] || $levels_left->[$current_level] == 0) { #~ $previous_level_separator .= "$color_start $color_end" ; $previous_level_separator .= "$color_start$glyphs->[3]$color_end" ; } else { #~ $previous_level_separator .= "$color_start| $color_end" ; $previous_level_separator .= "$color_start$glyphs->[0]$color_end" ; } } my $separator = '' ; my $subsequent_separator = '' ; $separator_size += 3 ; if($level > 0 || $start_level) { if($colors) { if('ARRAY' eq ref $colors) { my $color_index = $level % @{$colors->[0]} ; ($color_start, $color_end) = ($colors->[0][$color_index] , $colors->[1]) ; } else { # assume code ($color_start, $color_end) = $colors->($level) ; } } if($is_last_in_level == 0) { #~ $separator = "$color_start`- $color_end" ; #~ $subsequent_separator = "$color_start $color_end" ; $separator = "$color_start$glyphs->[2]$color_end" ; $subsequent_separator = "$color_start$glyphs->[3]$color_end" ; } else { #~ $separator = "$color_start|- $color_end" ; #~ $subsequent_separator = "$color_start| $color_end" ; $separator = "$color_start$glyphs->[1]$color_end" ; $subsequent_separator = "$color_start$glyphs->[0]$color_end" ; } } return ( $previous_level_separator , $separator , $subsequent_separator , $separator_size ) ; } #------------------------------------------------------------------------------- sub GetCallerStack { my $level_to_dump = shift || 1_000_000; my $current_level = 2 ; # skip this function $level_to_dump += $current_level ; # my @stack_dump ; while ($current_level < $level_to_dump) { my ($package, $filename, $line, $subroutine, $has_args, $wantarray, $evaltext, $is_require, $hints, $bitmask) = eval " package DB ; caller($current_level) ;" ; last unless defined $package; my %stack ; $stack{$subroutine}{EVAL} = 'yes' if($subroutine eq '(eval)') ; $stack{$subroutine}{EVAL} = $evaltext if defined $evaltext ; $stack{$subroutine}{ARGS} = [@DB::args] if($has_args) ; $stack{$subroutine}{'REQUIRE-USE'} = 'yes' if $is_require ; $stack{$subroutine}{CONTEXT} = defined $wantarray ? $wantarray ? 'list' : 'scalar' : 'void' ; $stack{$subroutine}{CALLERS_PACKAGE} = $package ; $stack{$subroutine}{AT} = "$filename:$line" ; unshift @stack_dump, \%stack ; $current_level++; } # usage example #~ print DumpTree #~ ( #~ (GetCallerStack())->[4] #~ , 'Stack dump:' #~ , DISPLAY_ADDRESS => 1 #~ , MAX_DEPTH => 5 #~ , DISPLAY_OBJECT_TYPE => 1 #~ , USE_ASCII => 1 #~ , QUOTE_VALUES => 1 #~ ) ; return(\@stack_dump); } #------------------------------------------------------------------------------- 1 ; __END__ =head1 NAME Data::TreeDumper - Improved replacement for Data::Dumper. Powerful filtering capability. =head1 SYNOPSIS use Data::TreeDumper ; my $sub = sub {} ; my $s = { A => { a => { } , bbbbbb => $sub , c123 => $sub , d => \$sub } , C => { b => { a => { a => { } , b => sub { } , c => 42 } } } , ARRAY => [qw(elment_1 element_2 element_3)] } ; #------------------------------------------------------------------- # package setup data #------------------------------------------------------------------- $Data::TreeDumper::Useascii = 0 ; $Data::TreeDumper::Maxdepth = 2 ; print DumpTree($s, 'title') ; print DumpTree($s, 'title', MAX_DEPTH => 1) ; print DumpTrees ( [$s, "title", MAX_DEPTH => 1] , [$s2, "other_title", DISPLAY_ADDRESS => 0] , USE_ASCII => 1 , MAX_DEPTH => 5 ) ; =head1 Output title: |- A [H1] | |- a [H2] | |- bbbbbb = CODE(0x8139fa0) [C3] | |- c123 [C4 -> C3] | `- d [R5] | `- REF(0x8139fb8) [R5 -> C3] |- ARRAY [A6] | |- 0 [S7] = elment_1 | |- 1 [S8] = element_2 | `- 2 [S9] = element_3 `- C [H10] `- b [H11] `- a [H12] |- a [H13] |- b = CODE(0x81ab130) [C14] `- c [S15] = 42 =head1 DESCRIPTION Data::Dumper and other modules do a great job of dumping data structures. Their output, however, often takes more brain power to understand than the data itself. When dumping large amounts of data, the output can be overwhelming and it can be difficult to see the relationship between each piece of the dumped data. Data::TreeDumper also dumps data in a tree-like fashion but I in a format more easily understood. =head2 Label Each node in the tree has a label. The label contains a type and an address. The label is displayed to the right of the entry name within square brackets. | |- bbbbbb = CODE(0x8139fa0) [C3] | |- c123 [C4 -> C3] | `- d [R5] | `- REF(0x8139fb8) [R5 -> C3] =head3 Address The addresses are linearly incremented which should make it easier to locate data. If the entry is a reference to data already displayed, a B<->> followed with the address of the already displayed data is appended within the label. ex: c123 [C4 -> C3] ^ ^ | | address of the data refered to | | current element address =head3 Types B: Scalar, B: Hash, B: Array, B: Code, B: Reference, B: Scalar reference. B: Object, where x is the object undelying type =head2 Empty Hash or Array No structure is displayed for empty hashes or arrays, the string "no elements" is added to the display. |- A [S10] = string |- EMPTY_ARRAY (no elements) [A11] |- B [S12] = 123 =head1 Configuration and Overrides Data::TreeDumper has configuration options you can set to modify the output it generates. I and I take overrides as trailing arguments. Those overrides are active within the current dump call only. ex: $Data::TreeDumper::Maxdepth = 2 ; # maximum depth set to 1 for the duration of the call only print DumpTree($s, 'title', MAX_DEPTH => 1) ; PrintTree($s, 'title', MAX_DEPTH => 1) ; # shortcut for the above call # maximum depth is 2 print DumpTree($s, 'title') ; =head2 $Data::TreeDumper::Displaycallerlocation This package variable is very usefull when you use B and don't know where you called B or B, ie when debugging. It displays the filename and line of call on STDOUT. It can't also be set as an override, DISPLAY_CALLER_LOCATION => 1. =head2 NO_PACKAGE_SETUP Sometimes, the package setup you have is not what you want to use. resetting the variable, making a call and setting the variables back is borring. You can set B to 1 and I will ignore the package setup for the call. print Data::TreeDumper::DumpTree($s, "Using package data") ; print Data::TreeDumper::DumpTree($s, "Not Using package data", NO_PACKAGE_SETUP => 1) ; =head2 DISPLAY_ROOT_ADDRESS By default, B doesn't display the address of the root. DISPLAY_ROOT_ADDRESS => 1 # show the root address =head2 DISPLAY_ADDRESS When the dumped data is not self-referential, displaying the address of each node clutters the display. You can direct B to not display the node address by using: DISPLAY_ADDRESS => 0 =head2 DISPLAY_PATH Add the path of the element to the its address. DISPLAY_PATH => 1 ex: '- CopyOfARRAY [A39 -> A18 /{'ARRAY'}] =head2 DISPLAY_OBJECT_TYPE B displays the package in which an object is blessed. You can suppress this display by using: DISPLAY_OBJECT_TYPE => 0 =head2 DISPLAY_INHERITANCE B will display the inheritance hierarchy for the object: |- object = blessed in 'SuperObject' <- Potatoe [OH55] | `- Data = 0 [S56] =head2 DISPLAY_AUTOLOAD if set, B will tag the object type with '[A]' if the package has an AUTOLOAD function. |- object_with_autoload = blessed in '[A]SuperObjectWithAutoload' <- Potatoe <- [A] Vegetable [O58] | `- Data = 0 [S56] =head2 DISPLAY_TIE if DISPLAY_TIE is set, B will display which packae the variable is tied to. This works for hashes and arrays as well as for object which are based on hashes and arrays. |- tied_hash (tied to 'TiedHash') [H57] | `- x = 1 [S58] |- tied_hash_object = (tied to 'TiedHash') blessed in 'SuperObject' <- [A]Potatoe <- Vegetable [O59] | |- m1 = 1 [S60] | `- m2 = 2 [S61] =head2 PERL DATA Setting one of the options below will show internal perl data: Cells: <2234> HASH(0x814F20c) |- A1 [H1] <204> HASH(0x824620c) | `- VALUE [S2] = datadatadatadatadatadatadatadatadatadata <85> |- A8 [H11] <165> HASH(0x8243d68) | `- VALUE [S12] = C <46> `- C2 [H19] <165> HASH(0x8243dc0) `- VALUE [S20] = B <46> =head3 DISPLAY_PERL_SIZE Setting this option will show the size of the memory allocated for each element in the tree within angle brackets. DISPLAY_PERL_SIZE => 1 The excellent L is used to compute the size of the perl data. If you have deep circular data structures, expect the dump time to be slower, 50 times slower or more. =head3 DISPLAY_PERL_ADDRESS Setting this option will show the perl-address of the dumped data. DISPLAY_PERL_ADDRESS => 1 =head2 REPLACEMENT_LIST Scalars may contain non printable characters that you rather not see in a dump. One of the most common is "\r" embedded in text string from dos files. B, by default, replaces "\n" by '[\n]' and "\r" by '[\r]'. You can set REPLACEMENT_LIST to an array ref containing elements which are themselves array references. The first element is the character(s) to match and the second is the replacement. # a fancy and stricter replacement for \n and \r my $replacement = [ ["\n" => '[**Fancy \n replacement**]'], ["\r" => '\r'] ] ; print DumpTree($smed->{TEXT}, 'Text:', REPLACEMENT_LIST => $replacement) ; =head2 QUOTE_HASH_KEYS B and its package variable B<$Data::TreeDumper::Quotehashkeys> can be set if you wish to single quote the hash keys. Hash keys are not quoted by default. DumpTree(\$s, 'some data:', QUOTE_HASH_KEYS => 1) ; # output some data: `- REF(0x813da3c) [H1] |- 'A' [H2] | |- 'a' [H3] | |- 'b' [H4] | | |- 'a' = 0 [S5] =head2 QUOTE_VALUES B and its package variable B<$Data::TreeDumper::Quotevalues> can be set if you wish to single quote the scalar values. DumpTree(\$s, 'Cells:', QUOTE_VALUES=> 1) ; =head2 NO_NO_ELEMENTS If this option is set, PBS will not add 'no elements' to empty hashes and arrays =head2 NO_OUTPUT This option suppresses all output generated by Data::TreeDumper. This is useful when you want to iterate through your data structures and display the data yourself, manipulate the data structure, or do a search (see L below) =head2 Filters Data::TreeDumper can sort the tree nodes with a user defined subroutine. By default, hash keys are sorted. FILTER => \&ReverseSort FILTER => \&Data::TreeDumper::HashKeysSorter FILTER_ARGUMENT => ['your', 'arguments'] The filter routine is passed these arguments: =over 2 =item 1 - a reference to the node which is going to be displayed =item 2 - the nodes depth (this allows you to selectively display elements at a certain depth) =item 3 - the path to the reference from the start of the dump. =item 4 - an array reference containing the keys to be displayed (see filter chaining below) last argument can be undefined and can then be safely ignored. =item 5 - the dumpers setup =item 5 - the filter arguments (see below) =back The filter returns the node's type, an eventual new structure (see below) and a list of 'keys' to display. The keys are hash keys or array indexes. In Perl: ($tree_type, $replacement_tree, @nodes_to_display) = $your_filter->($tree, $level, $path, $nodes_to_display, $setup) ; Filter are not as complicated as they sound and they are very powerfull, especially when using the path argument. The path idea was given to me by another module writer but I forgot whom. If this writer will contact me, I will give him the proper credit. Lots of examples can be found in I and I'll be glad to help if you want to develop a specific filter. =head3 FILTER_ARGUMENT it is possible to pass arguments to your filter, passing a reference allows you to modify the arguments when the filter is run (that happends for each node). sub SomeSub { my $counter = 0 ; my $data_structure = {.....} ; DumpTree($data_structure, 'title', FILTER => \&CountNodes, FILTER_ARGUMENT => \$counter) ; print "\$counter = $counter\n" ; } sub CountNodes { my ($structure, $level, $path, $nodes_to_display, $setup, $counter) = @_ ; $$counter++ ; # remember to pass references if you want them to be changed by the filter return(DefaultNodesToDisplay($structure)) ; } =head3 Key removal Entries can be removed from the display by not returning their keys. my $s = {visible => '', also_visible => '', not_visible => ''} ; my $OnlyVisible = sub { my $s = shift ; if('HASH' eq ref $s) { return('HASH', undef, grep {! /^not_visible/} keys %$s) ; } return(Data::TreeDumper::DefaultNodesToDisplay($s)) ; } DumpTree($s, 'title', FILTER => $OnlyVisible) ; =head3 Label changing The label for a hash keys or an array index can be altered. This can be used to add visual information to the tree dump. Instead of returning the key name, return an array reference containing the key name and the label you want to display. You only need to return such a reference for the entries you want to change, thus a mix of scalars and array ref is acceptable. sub StarOnA { # hash entries matching /^a/i have '*' prepended my $tree = shift ; if('HASH' eq ref $tree) { my @keys_to_dump ; for my $key_name (keys %$tree) { if($key_name =~ /^a/i) { $key_name = [$key_name, "* $key_name"] ; } push @keys_to_dump, $key_name ; } return ('HASH', undef, @keys_to_dump) ; } return (Data::TreeDumper::DefaultNodesToDisplay($tree)) ; } print DumpTree($s, "Entries matching /^a/i have '*' prepended", FILTER => \&StarOnA) ; If you use an ANSI terminal, you can also change the color of the label. This can greatly improve visual search time. See the I