http://paperlined.org/dev/src/pl/wikipedia/wikiknots/list.pl

#!/usr/bin/env perl

    use strict;
    use warnings;

    use MediaWiki::Bot;
    use Data::Dumper;


my @INCLUDE_PAGES = map {s/#.*|^\s+|\s+$//s; $_} split /\n/, <<'EOF';
    Category:Knots
    Wikipedia:WikiProject Knots
    Wikipedia:WikiProject Knots/General
    Template:Knot-stub
    Template:WikiProject Knots
EOF

my %EXCLUDE_PAGES = map {s/#.*|^\s+|\s+$//s; $_,1} split /\n/, <<'EOF';
    Category:Knot theory
EOF



my $bot = new MediaWiki::Bot({
    host    => 'en.wikipedia.org',
});
my $ns = new MediaWiki::Namespaces($bot->get_namespace_names());

# starting condition
my @queue = (@INCLUDE_PAGES);

my %seen;
while (my $next = shift @queue) {
    $seen{$next}++;
    next unless ($next =~ /^Category:/);
    print STDERR "======== expanding $next ========\n";
    my @children = $bot->get_pages_in_category($next);
    foreach my $child ($bot->get_pages_in_category($next)) {
        $child = $ns->force_nontalk($child);
        next if (exists $EXCLUDE_PAGES{$child} || $seen{$child}++);
        if ($child =~ /^Category:/) {
            push(@queue, $child);
        }
    }
}

## scan template: transclusions
if (0) {
    foreach my $template (grep /^Template:/, keys %seen) {
        print STDERR "======== scanning $template ========\n";
        foreach my $page ($bot->list_transclusions($template)) {
            $page = $ns->force_nontalk($page);
            next if (exists $EXCLUDE_PAGES{$page} || $seen{$page}++);
        }
    }
}

print <<'EOF';
This page is intended to be used via [[Special:RecentChangesLinked/{{FULLPAGENAME}}|recent changes]], to function as a watchlist.

This list is generated from the script found on the talk page, but it mostly consists of the contents of [[:Category:Knots]].

EOF

foreach my $page (sort keys %seen) {
    my $colon = ($page =~ /^Category:/) ? ':' : '';
    next if ($page =~ /^User:/);
    print "* [[$colon$page]] ([[", $ns->force_talk($page), "|talk]])\n";
}

exit;



package MediaWiki::Namespaces;

    use strict;
    use warnings;

    use Data::Dumper;

# call this, and pass the result of MediaWiki::Bot::get_namespace_names() into this
sub new {
    my ($class, %namespace_hash) = @_;
    my %self = (
        map => \%namespace_hash,
        reverse_map => { reverse %namespace_hash },
        split_regexp => join("|", map {"$_"} values %namespace_hash),
    );
    return bless(\%self, $class);
}

sub split {
    my ($self, $pagename) = @_;
    $pagename =~ s/^($self->{split_regexp})://;
    return ($1 || '', $pagename);
}

sub join {
    my ($self, $ns, $page) = @_;
    return $ns ? "$ns:$page" : $page;
}

# If given a talk page, returns the associated non-talk page name.
# If given a non-talk page, returns it unchanged.
sub force_nontalk {
    my ($self, $pagename, $set_to) = @_;
    $set_to ||= 0;      # $set_to is optional
    my ($ns_name, $suffix) = $self->split($pagename);
    #print Dumper [$ns_name, $suffix]; exit;
    my $ns = $self->{reverse_map}{$ns_name};
    $ns = $ns & 0xFFFE | $set_to;
    my $new_ns = $self->{map}{$ns};
    return $self->join($new_ns, $suffix);
}

# The opposite of force_nontalk().
sub force_talk {
    my ($self, $pagename) = @_;
    return $self->force_nontalk($pagename, 1);
}

Generated by GNU enscript 1.6.4.