From 0ca76b6716f6d9c8d98569083ce7fd86069858b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Sat, 25 Oct 2025 07:37:48 +0200 Subject: [PATCH 1/6] Detect jq at build time Detect whether and where the jq utility is available at build time, so that it can be used in system tests. If the tool is not found, specific checks employing it will be skipped. (cherry picked from commit 273b4bbfd787e2942ec8395bc5232d9fc56fd9e1) --- bin/tests/system/isctest/vars/.ac_vars/JQ.in | 1 + configure.ac | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 bin/tests/system/isctest/vars/.ac_vars/JQ.in diff --git a/bin/tests/system/isctest/vars/.ac_vars/JQ.in b/bin/tests/system/isctest/vars/.ac_vars/JQ.in new file mode 100644 index 0000000000..156174fd06 --- /dev/null +++ b/bin/tests/system/isctest/vars/.ac_vars/JQ.in @@ -0,0 +1 @@ +@JQ@ diff --git a/configure.ac b/configure.ac index 88d90d6f67..a60ba181f1 100644 --- a/configure.ac +++ b/configure.ac @@ -327,8 +327,9 @@ AC_SUBST([PYTEST]) AM_CONDITIONAL([HAVE_PYTEST], [test -n "$PYTEST"]) # -# xsltproc is optional, it is used only by system test scripts. +# Optional utilities, only used by system tests. # +AC_PATH_PROG([JQ], [jq]) AC_PATH_PROG([XSLTPROC], [xsltproc]) # @@ -1643,6 +1644,7 @@ AC_CONFIG_FILES([bin/tests/Makefile bin/tests/system/isctest/vars/.ac_vars/TOP_BUILDDIR bin/tests/system/isctest/vars/.ac_vars/TOP_SRCDIR bin/tests/system/isctest/vars/.ac_vars/FSTRM_CAPTURE + bin/tests/system/isctest/vars/.ac_vars/JQ bin/tests/system/isctest/vars/.ac_vars/SHELL bin/tests/system/isctest/vars/.ac_vars/PYTHON bin/tests/system/isctest/vars/.ac_vars/PERL From 1f0ed3c47909db6560d790d7f06285261abc49ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Sat, 25 Oct 2025 07:37:48 +0200 Subject: [PATCH 2/6] Use jq in system tests inspecting JSON data Inspecting JSON data using grep is error-prone, overly lax in some ways, overly strict in others, and neither accurate nor expressive. Use jq for inspecting JSON data in the "statschannel" and "synthfromdnssec" system tests to address these deficiencies. (cherry picked from commit b494e02761e9996a6619ed31a17ff0b6f32647eb) --- bin/tests/system/statschannel/tests.sh | 10 +++++----- bin/tests/system/synthfromdnssec/tests.sh | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bin/tests/system/statschannel/tests.sh b/bin/tests/system/statschannel/tests.sh index 314eb0ab19..c535e85de2 100644 --- a/bin/tests/system/statschannel/tests.sh +++ b/bin/tests/system/statschannel/tests.sh @@ -734,19 +734,19 @@ _wait_for_transfers() { if [ $count != 1 ]; then return 1; fi fi - if [ "$PERL_JSON" ]; then + if [ "$PERL_JSON" ] && [ -x "$JQ" ]; then getxfrins json j$n || return 1 # We expect 4 transfers - count=$(grep -c -E '"state":"(Zone Transfer Request|First Data|Receiving AXFR Data)"' xfrins.json.j$n) + count=$("$JQ" '.views._default.xfrins | length' /dev/null || ret=1 + test $("${JQ}" ".nsstats.${synthesized}" <$json) -eq $count || ret=1 else - grep '"'$synthesized'":' $json >/dev/null && ret=1 + "${JQ}" -e '.nsstats | has("'"${synthesized}"'")' <$json >/dev/null && ret=1 fi n=$((n + 1)) if [ $ret != 0 ]; then echo_i "failed"; fi From f38cbbd56cc8f6861ae96ed065c356b4f3b06c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Sat, 25 Oct 2025 07:37:48 +0200 Subject: [PATCH 3/6] Detect xmllint at build time Detect whether and where the xmllint utility is available at build time, so that it can be used in system tests. If the tool is not found, specific checks employing it will be skipped. (cherry picked from commit 85773d4d210f4497f93322da8005419a271058a7) --- bin/tests/system/isctest/vars/.ac_vars/XMLLINT.in | 1 + configure.ac | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 bin/tests/system/isctest/vars/.ac_vars/XMLLINT.in diff --git a/bin/tests/system/isctest/vars/.ac_vars/XMLLINT.in b/bin/tests/system/isctest/vars/.ac_vars/XMLLINT.in new file mode 100644 index 0000000000..9364f0fb42 --- /dev/null +++ b/bin/tests/system/isctest/vars/.ac_vars/XMLLINT.in @@ -0,0 +1 @@ +@XMLLINT@ diff --git a/configure.ac b/configure.ac index a60ba181f1..d403f37ef1 100644 --- a/configure.ac +++ b/configure.ac @@ -330,6 +330,7 @@ AM_CONDITIONAL([HAVE_PYTEST], [test -n "$PYTEST"]) # Optional utilities, only used by system tests. # AC_PATH_PROG([JQ], [jq]) +AC_PATH_PROG([XMLLINT], [xmllint]) AC_PATH_PROG([XSLTPROC], [xsltproc]) # @@ -1650,6 +1651,7 @@ AC_CONFIG_FILES([bin/tests/Makefile bin/tests/system/isctest/vars/.ac_vars/PERL bin/tests/system/isctest/vars/.ac_vars/CURL bin/tests/system/isctest/vars/.ac_vars/NC + bin/tests/system/isctest/vars/.ac_vars/XMLLINT bin/tests/system/isctest/vars/.ac_vars/XSLTPROC bin/tests/system/isctest/vars/.ac_vars/PYTEST bin/tests/system/dyndb/driver/Makefile From 4f74c89b42f46935486a37b4f0413ae5e832e8ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Sat, 25 Oct 2025 07:37:48 +0200 Subject: [PATCH 4/6] Use xmllint in system tests inspecting XML data Inspecting XML data using sed and grep is error-prone, overly lax in some ways, overly strict in others, and neither accurate nor expressive. Use xmllint and XPath expressions for inspecting XML data in the "statistics", "statschannel", and "synthfromdnssec" system tests to address these deficiencies. (cherry picked from commit 5872000d9ee352f1ab23c51a0cc537fb5546b603) --- bin/tests/system/statistics/tests.sh | 40 +++++++++++------------ bin/tests/system/statschannel/tests.sh | 11 +++---- bin/tests/system/synthfromdnssec/tests.sh | 19 +++++------ 3 files changed, 32 insertions(+), 38 deletions(-) diff --git a/bin/tests/system/statistics/tests.sh b/bin/tests/system/statistics/tests.sh index a840d9e27b..a0364057c1 100644 --- a/bin/tests/system/statistics/tests.sh +++ b/bin/tests/system/statistics/tests.sh @@ -150,11 +150,11 @@ n=$((n + 1)) ret=0 echo_i "checking that zones with slash are properly shown in XML output ($n)" -if $FEATURETEST --have-libxml2 && [ -x ${CURL} ]; then +if $FEATURETEST --have-libxml2 && [ -x "${CURL}" ] && [ -x "${XMLLINT}" ]; then ${CURL} http://10.53.0.1:${EXTRAPORT1}/xml/v3/zones >curl.out.${n} 2>/dev/null || ret=1 - grep '' curl.out.${n} >/dev/null || ret=1 + test -n "$("$XMLLINT" --xpath '/statistics/views/view[@name="_default"]/zones/zone[@name="32/1.0.0.127-in-addr.example"]' curl.out.${n})" || ret=1 else - echo_i "skipping test as libxml2 and/or curl was not found" + echo_i "skipping test as libxml2 and/or curl and/or xmllint was not found" fi if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) @@ -162,11 +162,11 @@ n=$((n + 1)) ret=0 echo_i "checking that zones return their type ($n)" -if $FEATURETEST --have-libxml2 && [ -x ${CURL} ]; then +if $FEATURETEST --have-libxml2 && [ -x "${CURL}" ] && [ -x "${XMLLINT}" ]; then ${CURL} http://10.53.0.1:${EXTRAPORT1}/xml/v3/zones >curl.out.${n} 2>/dev/null || ret=1 - grep 'primary' curl.out.${n} >/dev/null || ret=1 + test -n "$("$XMLLINT" --xpath '/statistics/views/view[@name="_default"]/zones/zone[@name="32/1.0.0.127-in-addr.example"]/type[text()="primary"]' curl.out.${n})" || ret=1 else - echo_i "skipping test as libxml2 and/or curl was not found" + echo_i "skipping test as libxml2 and/or curl and/or xmllint was not found" fi if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) @@ -230,23 +230,23 @@ n=$((n + 1)) ret=0 echo_i "checking bind9.xml socket statistics ($n)" -if $FEATURETEST --have-libxml2 && [ -e stats.xml.out ] && [ -x "${XSLTPROC}" ]; then +if $FEATURETEST --have-libxml2 && [ -e stats.xml.out ] && [ -x "${XSLTPROC}" ] && [ -x "${XMLLINT}" ]; then # Socket statistics (expect no errors) - grep "0" stats.xml.out >/dev/null || ret=1 - grep "0" stats.xml.out >/dev/null || ret=1 - grep "0" stats.xml.out >/dev/null || ret=1 - grep "0" stats.xml.out >/dev/null || ret=1 - grep "0" stats.xml.out >/dev/null || ret=1 - # grep "0" stats.xml.out >/dev/null || ret=1 + [ "$("$XMLLINT" --xpath 'count(/statistics/server/counters[@type="sockstat"]/counter[@name="TCP4AcceptFail" and text()="0"])' stats.xml.out)" -eq 1 ] || ret=1 + [ "$("$XMLLINT" --xpath 'count(/statistics/server/counters[@type="sockstat"]/counter[@name="TCP4BindFail" and text()="0"])' stats.xml.out)" -eq 1 ] || ret=1 + [ "$("$XMLLINT" --xpath 'count(/statistics/server/counters[@type="sockstat"]/counter[@name="TCP4ConnFail" and text()="0"])' stats.xml.out)" -eq 1 ] || ret=1 + [ "$("$XMLLINT" --xpath 'count(/statistics/server/counters[@type="sockstat"]/counter[@name="TCP4OpenFail" and text()="0"])' stats.xml.out)" -eq 1 ] || ret=1 + [ "$("$XMLLINT" --xpath 'count(/statistics/server/counters[@type="sockstat"]/counter[@name="TCP4RecvErr" and text()="0"])' stats.xml.out)" -eq 1 ] || ret=1 + # [ "$("$XMLLINT" --xpath 'count(/statistics/server/counters[@type="sockstat"]/counter[@name="TCP4SendErr" and text()="0"])' stats.xml.out)" -eq 1 ] || ret=1 - grep "0" stats.xml.out >/dev/null || ret=1 - grep "0" stats.xml.out >/dev/null || ret=1 - grep "0" stats.xml.out >/dev/null || ret=1 - grep "0" stats.xml.out >/dev/null || ret=1 - grep "0" stats.xml.out >/dev/null || ret=1 - grep "0" stats.xml.out >/dev/null || ret=1 + [ "$("$XMLLINT" --xpath 'count(/statistics/server/counters[@type="sockstat"]/counter[@name="TCP6AcceptFail" and text()="0"])' stats.xml.out)" -eq 1 ] || ret=1 + [ "$("$XMLLINT" --xpath 'count(/statistics/server/counters[@type="sockstat"]/counter[@name="TCP6BindFail" and text()="0"])' stats.xml.out)" -eq 1 ] || ret=1 + [ "$("$XMLLINT" --xpath 'count(/statistics/server/counters[@type="sockstat"]/counter[@name="TCP6ConnFail" and text()="0"])' stats.xml.out)" -eq 1 ] || ret=1 + [ "$("$XMLLINT" --xpath 'count(/statistics/server/counters[@type="sockstat"]/counter[@name="TCP6OpenFail" and text()="0"])' stats.xml.out)" -eq 1 ] || ret=1 + [ "$("$XMLLINT" --xpath 'count(/statistics/server/counters[@type="sockstat"]/counter[@name="TCP6RecvErr" and text()="0"])' stats.xml.out)" -eq 1 ] || ret=1 + [ "$("$XMLLINT" --xpath 'count(/statistics/server/counters[@type="sockstat"]/counter[@name="TCP6SendErr" and text()="0"])' stats.xml.out)" -eq 1 ] || ret=1 else - echo_i "skipping test as libxml2 and/or stats.xml.out file and/or xsltproc was not found" + echo_i "skipping test as libxml2 and/or stats.xml.out file and/or xsltproc and/or xmllint was not found" fi if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) diff --git a/bin/tests/system/statschannel/tests.sh b/bin/tests/system/statschannel/tests.sh index c535e85de2..8a343fe1b5 100644 --- a/bin/tests/system/statschannel/tests.sh +++ b/bin/tests/system/statschannel/tests.sh @@ -715,22 +715,19 @@ status=$((status + ret)) n=$((n + 1)) _wait_for_transfers() { - if [ "$PERL_XML" ]; then + if [ "$PERL_XML" ] && [ -x "$XMLLINT" ]; then getxfrins xml x$n || return 1 - # XML is encoded in one line, use awk to separate each transfer - # with a newline - # We expect 4 transfers - count=$(awk '{ gsub("(Zone Transfer Request|First Data|Receiving AXFR Data)') + count=$("$XMLLINT" --xpath 'count(/statistics/views/view[@name="_default"]/xfrins/xfrin)' xfrins.xml.x$n) if [ $count != 4 ]; then return 1; fi # We expect 3 of 4 to be retransfers - count=$(awk '{ gsub("No') + count=$("$XMLLINT" --xpath 'count(/statistics/views/view[@name="_default"]/xfrins/xfrin[firstrefresh[text()="No"]])' xfrins.xml.x$n) if [ $count != 3 ]; then return 1; fi # We expect 1 of 4 to be a new transfer - count=$(awk '{ gsub("Yes') + count=$("$XMLLINT" --xpath 'count(/statistics/views/view[@name="_default"]/xfrins/xfrin[firstrefresh[text()="Yes"]])' xfrins.xml.x$n) if [ $count != 1 ]; then return 1; fi fi diff --git a/bin/tests/system/synthfromdnssec/tests.sh b/bin/tests/system/synthfromdnssec/tests.sh index 7d011ce7e3..2d45e5ba63 100644 --- a/bin/tests/system/synthfromdnssec/tests.sh +++ b/bin/tests/system/synthfromdnssec/tests.sh @@ -715,7 +715,7 @@ for ns in 2 4 5 6; do status=$((status + ret)) done - if ${FEATURETEST} --have-libxml2 && [ -x "${CURL}" ]; then + if ${FEATURETEST} --have-libxml2 && [ -x "${CURL}" ] && [ -x "${XMLLINT}" ]; then echo_i "getting XML statisistcs for (synth-from-dnssec ${description};) ($n)" ret=0 xml=xml.out$n @@ -726,10 +726,9 @@ for ns in 2 4 5 6; do echo_i "check XML for 'CoveringNSEC' with (synth-from-dnssec ${description};) ($n)" ret=0 - counter=$(sed -n 's;.*.*\([0-9]*\).*0<" | wc -l) + zero=$("${XMLLINT}" --xpath 'count(/statistics/views/view[@name="_default"]/counters[@type="cachestats"]/counter[@name="CoveringNSEC" and text()="0"])' $xml) if [ ${synth} = yes ]; then test $zero = 0 || ret=1 else @@ -741,10 +740,9 @@ for ns in 2 4 5 6; do echo_i "check XML for 'CacheNSECNodes' with (synth-from-dnssec ${description};) ($n)" ret=0 - counter=$(sed -n 's;.*.*\([0-9]*\).*0<" | wc -l) + zero=$("${XMLLINT}" --xpath 'count(/statistics/views/view[@name="_default"]/counters[@type="cachestats"]/counter[@name="CacheNSECNodes" and text()="0"])' $xml) if [ ${ad} = yes ]; then test $zero = 0 || ret=1 else @@ -763,11 +761,10 @@ for ns in 2 4 5 6; do echo_i "check XML for '$synthesized}' with (synth-from-dnssec ${description};) ($n)" ret=0 - if [ ${synth} = yes ]; then - grep ''$count'' $xml >/dev/null || ret=1 - else - grep ''0'' $xml >/dev/null || ret=1 + if [ ${synth} != yes ]; then + count=0 fi + test $("${XMLLINT}" --xpath '/statistics/server/counters[@type="nsstat"]/counter[@name="'"${synthesized}"'"]/text()' $xml) -eq $count || ret=1 n=$((n + 1)) if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) From 33af4c042fd444973f6ff09839caf85a84322e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Sat, 25 Oct 2025 07:37:48 +0200 Subject: [PATCH 5/6] Remove unused xmllint-html.sh script There are no longer any HTML files in the BIND 9 source repository. Remove the xmllint-html.sh script that was used in the past to check those for errors. (cherry picked from commit d08addc2be557cdf6e76a559d415edc35ef61adb) --- .gitlab-ci.yml | 1 - util/xmllint-html.sh | 22 ---------------------- 2 files changed, 23 deletions(-) delete mode 100644 util/xmllint-html.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f5c4f53ea1..593d47817a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -673,7 +673,6 @@ misc: - sh util/check-trailing-whitespace.sh - if git grep SYSTEMTESTTOP -- ':!.gitlab-ci.yml'; then echo 'Please use relative paths instead of $SYSTEMTESTTOP.'; exit 1; fi - bash util/unused-headers.sh - - bash util/xmllint-html.sh # Check dangling symlinks in the repository - if find . -xtype l | grep .; then exit 1; fi needs: [] diff --git a/util/xmllint-html.sh b/util/xmllint-html.sh deleted file mode 100644 index 9b0a21c9e2..0000000000 --- a/util/xmllint-html.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh -f - -# Copyright (C) Internet Systems Consortium, Inc. ("ISC") -# -# SPDX-License-Identifier: MPL-2.0 -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, you can obtain one at https://mozilla.org/MPL/2.0/. -# -# See the COPYRIGHT file distributed with this work for additional -# information regarding copyright ownership. - -files=$(git ls-files '*.html') -if test -n "$files"; then - xmllint --noout --nonet --html $files 2>&1 \ - | awk 'BEGIN { status = 0; } - # suppress HTML 5
tag errors - /HTML parser error : Tag section invalid/ { getline; getline; next; } - { print; status = 1; } - END { exit status }' -fi From 92501f132027c40d7fc1abfcdd1e7cf721615384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20K=C4=99pie=C5=84?= Date: Sat, 25 Oct 2025 07:37:48 +0200 Subject: [PATCH 6/6] Remove unused Perl scripts The traffic-json.pl and traffic-xml.pl scripts that were used in the "statschannel" system test in the past became dead code when commit 1202fd912a1baa9c299f17caf4494bc21234da85 rewrote parts of that test to Python. Remove those scripts. (cherry picked from commit 5110dbacb998d58547a6721f7e069fcbee7327d6) --- bin/tests/system/statschannel/traffic-json.pl | 49 ------------------- bin/tests/system/statschannel/traffic-xml.pl | 46 ----------------- 2 files changed, 95 deletions(-) delete mode 100644 bin/tests/system/statschannel/traffic-json.pl delete mode 100644 bin/tests/system/statschannel/traffic-xml.pl diff --git a/bin/tests/system/statschannel/traffic-json.pl b/bin/tests/system/statschannel/traffic-json.pl deleted file mode 100644 index 353d6c761f..0000000000 --- a/bin/tests/system/statschannel/traffic-json.pl +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/perl - -# Copyright (C) Internet Systems Consortium, Inc. ("ISC") -# -# SPDX-License-Identifier: MPL-2.0 -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, you can obtain one at https://mozilla.org/MPL/2.0/. -# -# See the COPYRIGHT file distributed with this work for additional -# information regarding copyright ownership. - -# traffic-json.pl: -# Parses the JSON version of the RSSAC002 traffic stats into a -# normalized format. - -use JSON; - -my $file = $ARGV[0]; -open(INPUT, "<$file"); -my $text = do{local$/;}; -close(INPUT); - -my $ref = decode_json($text); - -my $tcprcvd = $ref->{traffic}->{"dns-tcp-requests-sizes-received-ipv4"}; -my $type = "tcp request-size "; -foreach $key (keys %{$tcprcvd}) { - print $type . $key . ": ". $tcprcvd->{$key} ."\n"; -} - -my $tcpsent = $ref->{traffic}->{"dns-tcp-responses-sizes-sent-ipv4"}; -my $type = "tcp response-size "; -foreach $key (keys %{$tcpsent}) { - print $type . $key . ": ". $tcpsent->{$key} ."\n"; -} - -my $udprcvd = $ref->{traffic}->{"dns-udp-requests-sizes-received-ipv4"}; -my $type = "udp request-size "; -foreach $key (keys %{$udprcvd}) { - print $type . $key . ": ". $udprcvd->{$key} ."\n"; -} - -my $udpsent = $ref->{traffic}->{"dns-udp-responses-sizes-sent-ipv4"}; -my $type = "udp response-size "; -foreach $key (keys %{$udpsent}) { - print $type . $key . ": ". $udpsent->{$key} ."\n"; -} diff --git a/bin/tests/system/statschannel/traffic-xml.pl b/bin/tests/system/statschannel/traffic-xml.pl deleted file mode 100644 index 5552cc5be9..0000000000 --- a/bin/tests/system/statschannel/traffic-xml.pl +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/perl - -# Copyright (C) Internet Systems Consortium, Inc. ("ISC") -# -# SPDX-License-Identifier: MPL-2.0 -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, you can obtain one at https://mozilla.org/MPL/2.0/. -# -# See the COPYRIGHT file distributed with this work for additional -# information regarding copyright ownership. - -# traffic-xml.pl: -# Parses the XML version of the RSSAC002 traffic stats into a -# normalized format. - -use XML::Simple; - -my $file = $ARGV[0]; - -my $ref = XMLin($file); - -my $udp = $ref->{traffic}->{ipv4}->{udp}->{counters}; -foreach $group (@$udp) { - my $type = "udp " . $group->{type} . " "; - if (exists $group->{counter}->{name}) { - print $type . $group->{counter}->{name} . ": " . $group->{counter}->{content} . "\n"; - } else { - foreach $key (keys %{$group->{counter}}) { - print $type . $key . ": ". $group->{counter}->{$key}->{content} ."\n"; - } - } -} - -my $tcp = $ref->{traffic}->{ipv4}->{tcp}->{counters}; -foreach $group (@$tcp) { - my $type = "tcp " . $group->{type} . " "; - if (exists $group->{counter}->{name}) { - print $type . $group->{counter}->{name} . ": " . $group->{counter}->{content} . "\n"; - } else { - foreach $key (keys %{$group->{counter}}) { - print $type . $key . ": ". $group->{counter}->{$key}->{content} ."\n"; - } - } -}