The Perl Toolchain Summit 2025 Needs You: You can help 🙏 Learn more

our $DATE = '2021-01-16'; # DATE
our $VERSION = '0.004009'; # VERSION
#IFUNBUILT
# # use strict 'subs', 'vars';
# # use warnings;
#END IFUNBUILT
my $has_globstar;
# do our own exporting to start faster
sub import {
my $pkg = shift;
my $caller = caller;
for my $sym (@_) {
if ($sym eq 'list_modules') { *{"$caller\::$sym"} = \&{$sym} }
else { die "$sym is not exported!" }
}
}
sub list_modules($$) {
my($prefix, $options) = @_;
my $trivial_syntax = $options->{trivial_syntax};
my($root_leaf_rx, $root_notleaf_rx);
my($notroot_leaf_rx, $notroot_notleaf_rx);
if($trivial_syntax) {
$root_leaf_rx = $notroot_leaf_rx = qr#:?(?:[^/:]+:)*[^/:]+:?#;
$root_notleaf_rx = $notroot_notleaf_rx =
qr#:?(?:[^/:]+:)*[^/:]+#;
} else {
$root_leaf_rx = $root_notleaf_rx = qr/[a-zA-Z_][0-9a-zA-Z_]*/;
$notroot_leaf_rx = $notroot_notleaf_rx = qr/[0-9a-zA-Z_]+/;
}
my $recurse = $options->{recurse};
# filter by wildcard. we cannot do this sooner because wildcard can be put
# at the end or at the beginning (e.g. '*::Path') so we still need
my $re_wildcard;
if ($options->{wildcard}) {
my $orig_prefix = $prefix;
my @prefix_parts = split /::/, $prefix;
$prefix = "";
my $has_wildcard;
while (defined(my $part = shift @prefix_parts)) {
if (String::Wildcard::Bash::contains_wildcard($part)) {
$has_wildcard++;
# XXX limit recurse level to scalar(@prefix_parts), or -1 if has_globstar
$recurse = 1 if @prefix_parts;
last;
} else {
$prefix .= "$part\::";
}
}
if ($has_wildcard) {
$re_wildcard = convert_wildcard_to_re($orig_prefix);
}
$recurse = 1 if $has_globstar;
}
die "bad module name prefix `$prefix'"
unless $prefix =~ /\A(?:${root_notleaf_rx}::
(?:${notroot_notleaf_rx}::)*)?\z/x &&
$prefix !~ /(?:\A|[^:]::)\.\.?::/;
my $list_modules = $options->{list_modules};
my $list_prefixes = $options->{list_prefixes};
my $list_pod = $options->{list_pod};
my $use_pod_dir = $options->{use_pod_dir};
return {} unless $list_modules || $list_prefixes || $list_pod;
my $return_path = $options->{return_path};
my @prefixes = ($prefix);
my %seen_prefixes;
my %results;
while(@prefixes) {
my $prefix = pop(@prefixes);
my @dir_suffix = split(/::/, $prefix);
my $module_rx =
$prefix eq "" ? $root_leaf_rx : $notroot_leaf_rx;
my $pm_rx = qr/\A($module_rx)\.pmc?\z/;
my $pod_rx = qr/\A($module_rx)\.pod\z/;
my $dir_rx =
$prefix eq "" ? $root_notleaf_rx : $notroot_notleaf_rx;
$dir_rx = qr/\A$dir_rx\z/;
foreach my $incdir (@INC) {
my $dir = join("/", $incdir, @dir_suffix);
opendir(my $dh, $dir) or next;
while(defined(my $entry = readdir($dh))) {
if(($list_modules && $entry =~ $pm_rx) ||
($list_pod &&
$entry =~ $pod_rx)) {
my $key = $prefix.$1;
next if $re_wildcard && $key !~ $re_wildcard;
$results{$key} = $return_path ? "$dir/$entry" : undef
if $return_path || !exists($results{$key});
} elsif(($list_prefixes || $recurse) &&
($entry ne '.' && $entry ne '..') &&
$entry =~ $dir_rx &&
-d join("/", $dir,
$entry)) {
my $newmod = $prefix.$entry;
my $newpfx = $newmod."::";
next if exists $seen_prefixes{$newpfx};
$results{$newpfx} = $return_path ? "$dir/$entry/" : undef
if ($return_path || !exists($results{$newpfx})) && $list_prefixes;
push @prefixes, $newpfx if $recurse;
}
}
next unless $list_pod && $use_pod_dir;
$dir = join("/", $dir, "pod");
opendir($dh, $dir) or next;
while(defined(my $entry = readdir($dh))) {
if($entry =~ $pod_rx) {
my $key = $prefix.$1;
next if $re_wildcard && $key !~ $re_wildcard;
$results{$key} = $return_path ? "$dir/$entry" : undef;
}
}
}
}
# we cannot filter prefixes early with wildcard because we need to dig down
# first and that would've been prevented if we had a wildcard like *::Foo.
if ($list_prefixes && $re_wildcard) {
for my $k (keys %results) {
next unless $k =~ /::\z/;
(my $k_nocolon = $k) =~ s/::\z//;
delete $results{$k} unless $k =~ $re_wildcard || $k_nocolon =~ $re_wildcard;
}
}
return \%results;
}
sub convert_wildcard_to_re {
$has_globstar = 0;
my $re = _convert_wildcard_to_re(@_);
$re = qr/\A$re\z/;
#print "DEBUG: has_globstar=<$has_globstar>, re=$re\n";
$re;
}
# modified from String::Wildcard::Bash 0.040's convert_wildcard_to_re
sub _convert_wildcard_to_re {
my $opts = ref $_[0] eq 'HASH' ? shift : {};
my $str = shift;
my $opt_brace = $opts->{brace} // 1;
my @res;
my $p;
while ($str =~ /$String::Wildcard::Bash::RE_WILDCARD_BASH/g) {
my %m = %+;
if (defined($p = $m{bash_brace_content})) {
push @res, quotemeta($m{slashes_before_bash_brace}) if
$m{slashes_before_bash_brace};
if ($opt_brace) {
my @elems;
while ($p =~ /($String::Wildcard::Bash::re_bash_brace_element)(,|\z)/g) {
push @elems, $1;
last unless $2;
}
#use DD; dd \@elems;
push @res, "(?:", join("|", map {
convert_wildcard_to_re({
bash_brace => 0,
}, $_)} @elems), ")";
} else {
push @res, quotemeta($m{bash_brace});
}
} elsif (defined($p = $m{bash_joker})) {
if ($p eq '?') {
push @res, '[^:]';
} elsif ($p eq '*') {
push @res, '[^:]*';
} elsif ($p eq '**') {
$has_globstar++;
push @res, '.*';
}
} elsif (defined($p = $m{literal_brace_single_element})) {
push @res, quotemeta($p);
} elsif (defined($p = $m{bash_class})) {
# XXX no need to escape some characters?
push @res, $p;
} elsif (defined($p = $m{sql_joker})) {
push @res, quotemeta($p);
} elsif (defined($p = $m{literal})) {
push @res, quotemeta($p);
}
}
join "", @res;
}
1;
# ABSTRACT: A fork of Module::List that groks wildcard
__END__
=pod
=encoding UTF-8
=head1 NAME
Module::List::Wildcard - A fork of Module::List that groks wildcard
=head1 VERSION
This document describes version 0.004009 of Module::List::Wildcard (from Perl distribution Module-List-Wildcard), released on 2021-01-16.
=head1 SYNOPSIS
Use like you would L<Module::List>, e.g.:
use Module::List::Wildcard qw(list_modules);
$id_modules = list_modules("Data::ID::", { list_modules => 1});
$prefixes = list_modules("", { list_prefixes => 1, recurse => 1 });
Wildcard support:
$modules = list_modules("Module::P*", { wildcard => 1, list_modules => 1});
$modules = list_modules("Module::P**", { wildcard => 1, list_modules => 1});
C<all> option:
$modules = list_modules("Module::", { list_modules => 1, all => 1});
=head1 DESCRIPTION
This module is a fork of L<Module::List>. It's exactly like Module::List 0.004,
except for the following differences:
=over
=item * lower startup overhead (with some caveats)
It strips the usage of L<Exporter>, L<IO::Dir>, L<Carp>, L<File::Spec>, with the
goal of saving a few milliseconds (a casual test on my PC results in 11ms vs
39ms).
Path separator is hard-coded as C</>.
=item * Recognize C<wildcard> option
This boolean option can be set to true to recognize wildcard pattern in prefix.
Wildcard patterns such as jokers (C<?>, C<*>, C<**>), classes (C<[a-z]>), as
well as braces (C<{One,Two}>) are supported. C<**> implies recursive listing
(sets C<recurse> option to 1).
Examples:
list_modules("Module::P*", {wildcard=>1, list_modules=>1});
results in something like:
{
"Module::Patch" => undef,
"Module::Path" => undef,
"Module::Pluggable" => undef,
}
while:
list_modules("Module::P**", {wildcard=>1, list_modules=>1});
results in something like:
{
"Module::Patch" => undef,
"Module::Path" => undef,
"Module::Path::More" => undef,
"Module::Pluggable" => undef,
"Module::Pluggable::Object" => undef,
}
while:
list_modules("Module::**le", {wildcard=>1, list_modules=>1});
results in something like:
{
"Module::Depakable" => undef,
"Module::Install::Admin::Bundle" => undef,
"Module::Install::Admin::Makefile" => undef,
"Module::Install::Bundle" => undef,
"Module::Install::Makefile" => undef,
"Module::Pluggable" => undef,
}
=back
=for Pod::Coverage .+
=head1 HOMEPAGE
Please visit the project's homepage at L<https://metacpan.org/release/Module-List-Wildcard>.
=head1 SOURCE
=head1 BUGS
Please report any bugs or feature requests on the bugtracker website L<https://github.com/perlancar/perl-Module-List-Wildcard/issues>
When submitting a bug or request, please include a test-file or a
patch to an existing test-file that illustrates the bug or desired
feature.
=head1 SEE ALSO
L<Module::List>
L<Module::List::Tiny>
L<Module::List::More>, from which Module::List::Wildcard is actually forked
from. Module::List::More contains more features, including wildcard support. I'm
currently maintainng both modules. Eventually this duplication might be resolved
in the future.
=head1 AUTHOR
perlancar <perlancar@cpan.org>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2021, 2020, 2019 by perlancar@cpan.org.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut