#!/usr/local/bin/perl -T # # SPDX-License-Identifier: BSD-2-Clause # Copyright (c) 1996-2026 Wolfram Schneider <[email protected]> # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # # ports.cgi - search engine for FreeBSD ports use POSIX qw(strftime); use Time::Local; use JSON; use warnings; our $hsty_base; require "./cgi-style.pl"; our $t_style = qq` <style type="text/css"> h3 { font-size: 1.20em; border-bottom: thin solid black; max-width: 42em; } form#ports > input[name='query'] { text-align: center; } form#ports > input[name='query'] { width: 20em; } form#ports > input, form#ports > button, form#ports > select { font-size: large; } span.footer_links { font-size: small; } span.space { font-size: xx-small; } p#section_links, div#footer { max-width: 50em; } hr { margin-left: 0em; max-width: 50em; } a:link { text-decoration:none; } a:hover { text-decoration:underline; } table, th, td { border: 1px solid black; border-collapse: collapse; } th, td { padding-left: 0.5em; padding-right: 0.5em; } span#noscript { color: red; font-size: normal; font-weight: bold; } </style> <link rel="search" type="application/opensearchdescription+xml" href="https://www.freebsd.org/opensearch/ports.xml" title="FreeBSD Ports" /> `; my $no_javascript_warning = <<'EOF'; <span id="noscript"> <noscript> <p>Please enable JavaScript in your browser for sorting columns. Thanks!</p> </noscript> </span> EOF my $pkg_javascript = <<'EOF'; <script type="text/javascript"> let sort_directions = {}; // remember asc/desc per column function sort_table(column_index) { const table = document.getElementById("pkg-result"); const tbody = table.tBodies[0]; const rows = Array.from(tbody.rows); // toggle direction const current = sort_directions[column_index] || "asc"; const direction = current === "asc" ? "desc" : "asc"; sort_directions[column_index] = direction; rows.sort((a, b) => { const a_text = a.cells[column_index].textContent.trim(); const b_text = b.cells[column_index].textContent.trim(); cmp = a_text.localeCompare(b_text, undefined, { numeric: true, sensitivity: "base" }); return direction === "asc" ? cmp : -cmp; }); // re-append in sorted order rows.forEach(row => tbody.appendChild(row)); } // always sort by release document.addEventListener("DOMContentLoaded", function() { sort_table(0) }); </script> EOF # No unlimited result set. A HTML page with 1000 results can be 10MB big. my $max_hits = 1000; my $max_hits_default = 250; my $max_hits_pkg = 400; my $max; my $debug = 1; # feature flags my $enable_packages_link = 1; my $enable_check_freebsd_mailing_list = 1; my %lists; sub init_variables { $localPrefix = '/usr/ports'; # ports prefix # Directory of the up-to-date INDEX* $portsDatabaseHeadDir = "/usr/local/www/ports"; # Ports database file to use if ( -f "$portsDatabaseHeadDir/INDEX-15" ) { $ports_database = 'INDEX-15'; } elsif ( -f "$portsDatabaseHeadDir/INDEX-14" ) { $ports_database = 'INDEX-14'; } else { $ports_database = 'INDEX'; } # URL of ports tree for browsing $remotePrefixFtp = 'ports'; # Web interface for the Ports tree $remotePrefixRepo = 'https://cgit.FreeBSD.org/ports'; # visible E-Mail address, plain text $mailto = '[email protected]'; # Mailinglist for FreeBSD Ports $mailtoList = '[email protected]'; # use mailto:email?subject $mailtoAdvanced = 'yes'; # the URL if you click at the E-Mail address (see below) $mailtoURL = "mailto:$mailto" if !$mailtoURL; # security $ENV{'PATH'} = '/bin:/usr/bin'; } # return the date of the last ports database update sub last_update { local ($file) = "$portsDatabaseHeadDir/$ports_database"; local ( $modtime, $modtimestr ); $modtime = ( stat($file) )[9]; if ( defined($modtime) && $modtime > 0 ) { $modtimestr = strftime( "%Y-%m-%d %H:%M:%S UTC", gmtime($modtime) ); } else { $modtimestr = "Unknown"; } return $modtimestr; } sub last_update_message { return "Last database update: @{[ &last_update ]}\n"; } sub dec { local ($_) = @_; s/\+/ /g; # '+' -> space s/%(..)/pack("c",hex($1))/ge; # '%ab' -> char ab return ($_); } sub decode_form { local ( $form, *data ) = @_; foreach my $line ( split( /&/, $form ) ) { my ( $key, $val ) = split( /=/, $line, 2 ); $val = "" if !defined $val; $val =~ s/\+/ /g; # + -> space $key =~ s/\+/ /g; # + -> space $val =~ s/%([\da-f]{1,2})/pack(C,hex($1))/eig; # undo % escapes $key =~ s/%([\da-f]{1,2})/pack(C,hex($1))/eig; # undo % escapes $data{$key} = $val; } } sub escapeHTML { my $toencode = shift; return "" unless defined($toencode); $toencode =~ s{&}{&}gso; $toencode =~ s{<}{<}gso; $toencode =~ s{>}{>}gso; $toencode =~ s{"}{"}gso; return $toencode; } # encode unknown data for use in a URL <a href="..."> sub encode_url { local ($_) = @_; s/([\000-\032\;\/\?\:\@\&\=\%\'\"\`\<\>\177-\377 ])/sprintf('%%%02x',ord($1))/eg; # s/%20/+/g; $_; } sub env { defined( $ENV{ $_[0] } ) ? $ENV{ $_[0] } : undef; } sub exit { exit 0 } sub readindex { local ( *var, *msec ) = @_; local ($localportsdb) = "$portsDatabaseHeadDir/$ports_database"; local ( @tmp, @s ); open( C, $localportsdb ) || do { warn "Cannot open ports database $localportsdb: $!\n"; &exit; }; while (<C>) { next if $query && !/$query/oi; chop; @tmp = split(/\|/); $var{"$tmp[0]"} = $_; @s = split( /\s+/, $tmp[6] ); foreach (@s) { $msec{"$tmp[1],$_"} = 1; } } close C; } # extract sub collections sub readcoll { local ( @a, @b, %key ); local ($file) = '../ports/categories'; local ($localportsdb) = "$portsDatabaseHeadDir/$ports_database"; if ( -r $file && open( C, $file ) ) { while (<C>) { chop; if (/^\s*([^,]+),\s*"([^"]+)",\s*([A-Z]+)/) { @b = split( /\s+/, $1 ); foreach (@b) { if ( !defined( $key{$_} ) ) { $key{$_} = 1; } } } } } else { if ( -r $localportsdb ) { open( C, $localportsdb ) || do { warn "Cannot open ports database $localportsdb: $!\n"; &exit; } } while (<C>) { chop; @a = split('\|'); @b = split( /\s+/, $a[6] ); foreach (@b) { if ( !defined( $key{$_} ) ) { $key{$_} = 1; } } } } close C; @a = (); foreach ( sort keys %key ) { push( @a, $_ ); } return @a; } # basic function for HTML output sub out { local ($line) = @_; my ( $version, $path, $local, $comment, $descfile, $email, $sections, $bdepends, $rdepends, @rest ) = split( /\|/, $line ); if ( $path =~ m%^$localPrefix/([^/]+)%o ) { if ( !$out_sec || $1 ne $out_sec ) { print "</dl>\n" if $counter > 0; print qq{\n<h3>} . qq{<a href="$remotePrefixRepo/tree/$1">Category $1</a>} . "</h3>\n<dl>\n"; $out_sec = $1; } } $rdepends //= ""; $counter++; $pathB = $path; my $port_path = $path; $port_path =~ s,/usr/ports/,,; $pathB =~ s/^$localPrefix/ports/o; $path =~ s/^$localPrefix/$remotePrefixFtp/o; $descfile =~ s/^$localPrefix/$remotePrefixFtp/o; $version = &encode_url($version); $email = &check_freebsd_mailing_list($email) if $enable_check_freebsd_mailing_list; #$version =~ s/[\+,]/X/g; local ($l) = $path; $l =~ s%^$remotePrefixFtp%$remotePrefixRepo/log%o; local ($t) = $path; $t =~ s%^$remotePrefixFtp%$remotePrefixRepo/tree%o; $descfile =~ s%^$remotePrefixFtp%$remotePrefixRepo/plain%o; print qq{<dt><b><a name="$version"></a><a href="$t">$version</a></b></dt>\n}; print qq{<dd>}, &escapeHTML($comment), qq{<br />\n}; print qq[<a href="$descfile?revision=HEAD">Description</a>\n]; print qq[<b>:</b> <a href="$l">Changes</a>\n]; print qq[<b>:</b> <a href="?stype=pkg&query=], escapeHTML($port_path), qq[">Packages</a>\n] if $enable_packages_link; print qq[<br />\n]; print qq{<i>Maintained by:</i> <a href="mailto:$email} . ( $mailtoAdvanced ? qq{?cc=$mailtoList&subject=FreeBSD%20Port:%20} . &encode_url($version) : '' ) . qq{">$email</a><br />\n}; local (@s) = split( /\s+/, $sections ); if ( $#s > 0 ) { print qq{<i>Also listed in:</i> }; foreach (@s) { print qq{$_ } if $_ ne $out_sec; } print "<br />\n"; } if ( $bdepends || $rdepends ) { local ($flag) = 0; local ($last) = ''; print qq{<i>Requires:</i> }; foreach ( sort split( /\s+/, "$bdepends $rdepends" ) ) { # delete double entries next if $_ eq $last; $last = $_; print ", " if $flag; $flag++; print qq{<a href="$script_name?query=^$_&stype=name">$_</a>}; } print "<br />\n"; } print qq[</dd>]; # XXX: should be done in a CSS print qq[<dd> </dd>]; print qq[\n\n]; } # search and output sub package_links { my $q = shift; my $filter = shift; my @system = ( qw[env packagesize_dir=/usr/local/www/ports/packages /usr/local/www/bin/pkg-search.sh], ); push @system, $q; if ( !open( PKG_IN, '-|' ) ) { if ( !exec(@system) ) { die join( " ", @system ) . " $!\n"; } } binmode( PKG_IN, ":bytes" ); my $hash; my $counter = 0; while (<PKG_IN>) { chomp; next if !(m,^(.*?)-(.*?)\.yaml:(.*),); my $arch = $1; my $rel_major = $1; my $rel_minor = "$1/$2"; my $snapshot = $2; my $path = "$1/$2"; my $perl = decode_json($3); $arch =~ s,.*%3A,,; if ( $rel_major =~ /^FreeBSD%3A(\d+)%3A/ ) { $rel_major = ":$1:"; } if ( $rel_minor =~ /^FreeBSD%3A(\d+)%3A.*release_(\d+)$/ ) { $rel_minor = ":$1:$2"; } else { $rel_minor = ""; } if ( $. == 1 ) { print qq[<h2>$perl->{"name"}: ], escapeHTML( $perl->{"comment"} ), qq[</h2>\n]; print qq[homepage: <a href="], $perl->{"www"}, qq[">] . $perl->{"www"} . "</a><br/>\n"; print qq[FreeBSD ports git: <a href="$remotePrefixRepo/tree/] . $perl->{"origin"} . qq[">] . $perl->{"origin"} . qq[</a><br/>\n]; my $maintainer = $perl->{"maintainer"}; $maintainer = &check_freebsd_mailing_list($maintainer) if $enable_check_freebsd_mailing_list; print qq[maintainer: $maintainer<br/>\n]; print qq[<h3>Description</h3>\n]; print "<pre>", escapeHTML( $perl->{"desc"} ), "</pre>\n"; print qq[<h3>Download packages in *.pkg format</h3>\n]; print $no_javascript_warning, $pkg_javascript; print qq{<table id="pkg-result">\n}; print qq{<thead>\n}; print qq{<tr>\n}; print qq{ <th onclick="sort_table(0)" title="click to sort asc/desc by release">Release <></th>\n}; print qq{ <th onclick="sort_table(1)" title="click to sort asc/desc by version">Version <></th>\n}; print qq{ <th onclick="sort_table(2)" title="click to sort asc/desc by build time">Build Time <></th>\n}; print qq{</tr>\n}; print qq{</thead>\n}; print qq{<tbody>\n}; } my $release = $perl->{"abi"} . " " . $snapshot; my $time = $perl->{"annotations"}->{"build_timestamp"} // ""; my $version = $perl->{"version"}; my $repopath = $perl->{"repopath"}; my $flavor = $perl->{"annotations"}->{"flavor"} // ""; # show flavor if ($flavor) { $release = $release . " (" . $flavor . ")"; } $time =~ s/\+\d{4}$//; $time =~ s/T(\d\d):(\d\d):(\d\d)$/ $1:$2/; my $pkg_opt = $repopath; $pkg_opt =~ s,.*/,,; $pkg_opt =~ s,\~.*,,; if ($filter) { next if index( $release, $filter ) < 0 && index( $pkg_opt, $filter ) < 0 && index( $rel_minor, $filter ) < 0 && index( $version, $filter ) < 0 && index( $time, $filter ) < 0 && index( $flavor, $filter ) < 0; } next if $counter >= $max; $counter++; my $info = $time ? qq[pkg: $pkg_opt\n size: $perl->{"pkgsize"} bytes\n git_hash: $perl->{"annotations"}->{"port_git_hash"}\n ] . qq[ports_top_git_hash: $perl->{"annotations"}->{"ports_top_git_hash"}\nchecksum: $perl->{"sum"}\n] : ""; print "<tr>\n"; print "<td>", qq[<a href="https://pkg.freebsd.org/], escapeHTML($path), "/", escapeHTML($repopath), qq[">$release</a></td>\n]; print "<td>", $version, "</td>\n"; print qq[<td><span title="$info">], $time, "</span></td>\n"; print "</tr>\n"; $hash->{'version'}->{$version}++; $hash->{'arch'}->{$arch}++; $hash->{'release_major'}->{$rel_major}++; $hash->{'release_minor'}->{$rel_minor}++; $hash->{'flavor'}->{$flavor}++ if $flavor ne ""; $hash->{'snapshot'}->{$snapshot}++ if $snapshot eq 'latest' || $snapshot eq 'quarterly'; } if ( $counter || $. >= 1 ) { print <<EOF; </tbody> </table> <br/> EOF foreach my $key ( sort keys %$hash ) { print "<b>", escapeHTML($key), "</b>: \n"; my $flag = 0; foreach my $k ( sort keys %{ $hash->{$key} } ) { print " - " if $flag++; print qq[<a title="counter=], $hash->{$key}->{$k}, qq[" href="?stype=pkg&query=], escapeHTML($q), qq[&pkg_filter=], escapeHTML($k), qq[">], escapeHTML($k), "</a>\n"; } print "<br/>\n"; } if ( $filter ne "" ) { print escapeHTML(">>>"), qq[ <a href="?stype=pkg&query=], escapeHTML($q), qq[">reset filter</a> ], escapeHTML("<<<"), "\n"; } } return $counter; } # search and output sub search_ports { local (@a) = (); local ( $key, $name, $text ); foreach $key ( sort keys %today ) { next if $today{$key} !~ /$query/oi; next if $counter >= $max; @a = split( /\|/, $today{$key} ); $name = $a[0]; #$name =~ s/(\W)/\\$1/g; $text = $a[3]; #$text =~ s/(\W)/\\$1/g; if ( $section ne "all" ) { next if $a[6] !~ /\b$section\b/; } #warn "$stype:$query: $name $text\n"; if ( $stype eq "name" && $name =~ /$query/o ) { &out( $today{$key} ); } elsif ( $stype eq "text" && $text =~ /$query/oi ) { &out( $today{$key} ); } elsif ( $stype eq "all" && ( $text =~ /$query/oi || $name =~ /$query/io ) ) { &out( $today{$key} ); } elsif ( $stype eq 'maintainer' && $a[5] =~ /$query/io ) { &out( $today{$key} ); } elsif ( $stype eq 'requires' && ( defined $a[7] && $a[7] =~ /$query/io || ( defined $a[8] && $a[8] =~ /$query/io ) ) ) { &out( $today{$key} ); } } } sub forms { print qq{ <form id="ports" method="get" action="$script_name"> <input name="query" value="$query" type="text" autocapitalize="none" autofocus /> <select name="stype"> }; local (%d); %d = ( 'name', 'Package Name', 'all', 'All', 'maintainer', 'Maintainer', 'text', 'Description', 'requires', 'Requires', ); foreach ( 'all', 'name', 'text', 'maintainer', 'requires' ) { print "<option" . ( ( $_ eq $stype ) ? ' selected="selected" ' : ' ' ) . qq{value="$_">} . ( $d{$_} ? $d{$_} : $_ ) . qq{</option>\n}; } print qq{</select> <select name="sektion"> <option value="all">All Sections</option> }; foreach (@sec) { print "<option" . ( ( $_ eq $section ) ? ' selected="selected" ' : ' ' ) . qq{value="$_">$_</option>\n}; } print qq{</select> <input type="submit" value="Submit" /> </form> <br/> @{[ &footer_links ]} <hr noshade="noshade" /> }; } sub check_query { my ( $query, $sourceid ) = @_; $query =~ s/"/ /g; $query =~ s/^\s+//; $query =~ s/\s+$//; # XXX: Firefox opensearch autocomplete workarounds if ( $sourceid eq 'opensearch' ) { # remove space before a dot $query =~ s/ \././g; # remove space between double colon $query =~ s/: :/::/g; } return $query; } sub check_input { if ($query) { $stype = "all" if !$stype; if ( !( $stype eq "name" || $stype eq "text" || $stype eq "maintainer" || $stype eq "requires" || $stype eq "all" || $stype eq "pkg" ) ) { print "unknown search type, use `all', `text', `name', 'requires', or `maintainer'\n"; warn "unknown search type ``", escapeHTML($stype), "'', use `all', `text', `name', 'requires', or `maintainer'\n" if $debug >= 1; &exit(0); } } $max = int($max); if ( $max <= 0 || $max > $max_hits ) { my $old_max = $max; $max = $enable_packages_link && $stype eq 'pkg' ? $max_hits_pkg : $max_hits_default; warn "reset max=$old_max to $max\n"; } } sub help { print <<EOF; <br/> <h1>FreeBSD Ports Search Help</h1> <p> The FreeBSD Ports and Packages Collection offers a simple way for users and administrators to install applications. </p> <p> <b>Package Name</b> searches for the name of a port or distribution. <b>Description</b> searches case-insensitive in a short comment about the port. <b>All</b> searches case-insensitive for the package name and in the description about the port. <b>Maintainer</b> searches for the email address of the port maintainer. <b>Requires</b> searches for ports which depends on this port. </p> <h2>External Links</h2> <dl> <dt><b>Description</b></dt> <dd>A more detailed description (text) via the git repo</dd> <dt><b>Changes</b></dt> <dd>Read the latest changes via the git repo</dd> <dt><b>Packages</b></dt> <dd>List of available packages for all supported releases and branches</dd> </dl> <h2>Documentation</h2> <p> Handbook: <a href="https://docs.freebsd.org/en/books/handbook/ports/#ports-using">Using the Ports Collection</a> </p> <p> You may also search the <a href="https://man.FreeBSD.org/cgi/man.cgi?manpath=freebsd-ports">ports manual pages</a>. </p> <h2>Updates</h2> <p> The script ports.cgi use the file <a href="https://download.FreeBSD.org/ports/index/$ports_database.xz">$ports_database</a> as database for its operations. $ports_database is updated automatically every two hours. For other FreeBSD Releases INDEX files, please look at <a href="https://download.freebsd.org/ports/index/">https://download.freebsd.org/ports/index/</a> </p> <p> @{[ &last_update_message ]} </p> <h2>Copyright</h2> <pre> Copyright (c) 1996-2026 <a href="https://wolfram.schneider.org">Wolfram Schneider</a> <wosch\@FreeBSD.org> </pre> <p/> <h2>Misc</h2> <ul> <li><a href="https://forums.freebsd.org/categories/ports-and-packages.21/">FreeBSD Forums: Ports and Packages</a></li> <li><a href="https://www.freshports.org/">FreshPorts -- The Place For Ports - Most recent commits</a></li> </ul> <h2>Questions</h2> <p> General questions about FreeBSD ports should be sent to the <a href="https://lists.freebsd.org/subscription/freebsd-ports">$mailtoList</a> mailing list. </p> @{[ &footer_links ]} <hr noshade="noshade" /> EOF } sub footer_links { return <<EOF; <span class="footer_links"> <a href="$script_name">home</a> @{[ $stype eq "help" ? "" : qq, | <a href="$script_name?stype=help">help</a>, ]} </span> EOF } # expand FreeBSD mailing list aliases to full name, # e.g 'office' => 'freebsd-office' sub check_freebsd_mailing_list { my $email = shift; my ( $user, $hostname ) = split( '@', $email ); # email is a ports section, not a real user if ( $lists{$user} ) { $user = 'freebsd-' . $user; } return $user . '@' . $hostname; } # # Main # &init_variables; $query_string = &env('QUERY_STRING'); $path_info = &env('PATH_INFO') // ""; &decode_form( $query_string, *form ); $section = $form{'sektion'}; $section = 'all' if ( !$section ); $query = $form{'query'} // ""; $stype = $form{'stype'} // ""; $sourceid = $form{'sourceid'} // ""; $pkg_filter = $form{'pkg_filter'} // ""; $script_name = &env('SCRIPT_NAME'); $max = $form{'max'} // ( $enable_packages_link && $stype eq 'pkg' ? $max_hits_pkg : $max_hits_default ); if ( $path_info eq "/source" ) { print "Content-type: text/plain\n\n"; open( R, $0 ) || do { warn "open $0: $!!\n"; &exit; }; while (<R>) { print } close R; &exit; } if ( $stype eq "help" ) { print &short_html_header( "FreeBSD Ports Search Help", 1 ); &help; print &html_footer; &exit(0); } print &html_header( "FreeBSD Ports Search", 1 ); # allow `/ports.cgi?netscape' where 'netscape' is the query port to search # this make links to this script shorter if ( !$query && $query_string =~ /^([^=&]+)$/ ) { $query = $1; } # get all categories @sec = &readcoll; # mailing list aliases %lists = map { $_ => 1 } qw/apache chromium desktop elastic emulation enlightenment erlang fortran gecko gnome go haskell java multimedia office perl pkg ports python ruby tcltk tex uboot x11 xfce zope/; $query = &check_query( $query, $sourceid ); # no search menu for packages links if ( $enable_packages_link && $stype eq 'pkg' ) { } else { &forms; } if ( $query_string eq "" || !$query ) { print &html_footer; &exit(0); } &check_input; $counter = 0; # no prefix search for requires supported yet $query =~ s/^\^// if $stype eq 'requires'; # quote non characters my $query2 = $query; $query =~ s/([^\w\^])/\\$1/g; # search if ($query) { my $q = ""; if ( $query2 =~ m,^([A-Za-z0-9\-]+)/([A-Za-z0-9\-_\+\.\\]+)$, ) { $q = "$1/$2"; } if ( $q ne "" && $enable_packages_link && $stype eq 'pkg' ) { $counter = &package_links( $q, $pkg_filter ); } else { &readindex( *today, *msec ); &search_ports; } } if ( !$counter ) { print <<EOF; <p> Sorry, nothing found. You may look for other <a href="https://www.freebsd.org/search/">FreeBSD Search Services</a> </p> @{[ &footer_links ]} EOF } if ($counter) { print "</dl>\n" if $stype ne 'pkg'; my $counter_message = $counter; if ( $counter >= $max ) { $counter_message .= " (max hit limit reached)"; warn "$counter_message: query=$query stype=$stype section=$section\n" if $debug >= 1; } print "<p>Number of results: $counter_message\n</p>\n"; print &footer_links; } print qq{<hr noshade="noshade" />\n}; print &html_footer;