package Exporter::Tiny; use 5.006001; use strict; use warnings; no warnings qw(void once uninitialized numeric redefine); our $AUTHORITY = 'cpan:TOBYINK'; our $VERSION = '1.000000'; our @EXPORT_OK = qw< mkopt mkopt_hash _croak _carp >; sub _croak ($;@) { require Carp; my $fmt = shift; @_ = sprintf($fmt, @_); goto \&Carp::croak } sub _carp ($;@) { require Carp; my $fmt = shift; @_ = sprintf($fmt, @_); goto \&Carp::carp } my $_process_optlist = sub { my $class = shift; my ($global_opts, $opts, $want, $not_want) = @_; while (@$opts) { my $opt = shift @{$opts}; my ($name, $value) = @$opt; ($name =~ m{\A\!(/.+/[msixpodual]+)\z}) ? do { my @not = $class->_exporter_expand_regexp($1, $value, $global_opts); ++$not_want->{$_->[0]} for @not; } : ($name =~ m{\A\!(.+)\z}) ? (++$not_want->{$1}) : ($name =~ m{\A[:-](.+)\z}) ? push(@$opts, $class->_exporter_expand_tag($1, $value, $global_opts)) : ($name =~ m{\A/.+/[msixpodual]+\z}) ? push(@$opts, $class->_exporter_expand_regexp($name, $value, $global_opts)) : # else ? push(@$want, $opt); } }; sub import { my $class = shift; my $global_opts = +{ @_ && ref($_[0]) eq q(HASH) ? %{+shift} : () }; $global_opts->{into} = caller unless exists $global_opts->{into}; my @want; my %not_want; $global_opts->{not} = \%not_want; my @args = do { no strict qw(refs); @_ ? @_ : @{"$class\::EXPORT"} }; my $opts = mkopt(\@args); $class->$_process_optlist($global_opts, $opts, \@want, \%not_want); my $permitted = $class->_exporter_permitted_regexp($global_opts); $class->_exporter_validate_opts($global_opts); for my $wanted (@want) { next if $not_want{$wanted->[0]}; my %symbols = $class->_exporter_expand_sub(@$wanted, $global_opts, $permitted); $class->_exporter_install_sub($_, $wanted->[1], $global_opts, $symbols{$_}) for keys %symbols; } } sub unimport { my $class = shift; my $global_opts = +{ @_ && ref($_[0]) eq q(HASH) ? %{+shift} : () }; $global_opts->{into} = caller unless exists $global_opts->{into}; $global_opts->{is_unimport} = 1; my @want; my %not_want; $global_opts->{not} = \%not_want; my @args = do { our %TRACKED; @_ ? @_ : keys(%{$TRACKED{$class}{$global_opts->{into}}}) }; my $opts = mkopt(\@args); $class->$_process_optlist($global_opts, $opts, \@want, \%not_want); my $permitted = $class->_exporter_permitted_regexp($global_opts); $class->_exporter_validate_unimport_opts($global_opts); my $expando = $class->can('_exporter_expand_sub'); $expando = undef if $expando == \&_exporter_expand_sub; for my $wanted (@want) { next if $not_want{$wanted->[0]}; if ($wanted->[1]) { _carp("Passing options to unimport '%s' makes no sense", $wanted->[0]) unless (ref($wanted->[1]) eq 'HASH' and not keys %{$wanted->[1]}); } my %symbols = defined($expando) ? $class->$expando(@$wanted, $global_opts, $permitted) : ($wanted->[0] => sub { "dummy" }); $class->_exporter_uninstall_sub($_, $wanted->[1], $global_opts) for keys %symbols; } } # Called once per import/unimport, passed the "global" import options. # Expected to validate the options and carp or croak if there are problems. # Can also take the opportunity to do other stuff if needed. # sub _exporter_validate_opts { 1 } sub _exporter_validate_unimport_opts { 1 } # Called after expanding a tag or regexp to merge the tag's options with # any sub-specific options. # sub _exporter_merge_opts { my $class = shift; my ($tag_opts, $global_opts, @stuff) = @_; $tag_opts = {} unless ref($tag_opts) eq q(HASH); _croak('Cannot provide an -as option for tags') if exists $tag_opts->{-as} && ref $tag_opts->{-as} ne 'CODE'; my $optlist = mkopt(\@stuff); for my $export (@$optlist) { next if defined($export->[1]) && ref($export->[1]) ne q(HASH); my %sub_opts = ( %{ $export->[1] or {} }, %$tag_opts ); $sub_opts{-prefix} = sprintf('%s%s', $tag_opts->{-prefix}, $export->[1]{-prefix}) if exists($export->[1]{-prefix}) && exists($tag_opts->{-prefix}); $sub_opts{-suffix} = sprintf('%s%s', $export->[1]{-suffix}, $tag_opts->{-suffix}) if exists($export->[1]{-suffix}) && exists($tag_opts->{-suffix}); $export->[1] = \%sub_opts; } return @$optlist; } # Given a tag name, looks it up in %EXPORT_TAGS and returns the list of # associated functions. The default implementation magically handles tags # "all" and "default". The default implementation interprets any undefined # tags as being global options. # sub _exporter_expand_tag { no strict qw(refs); my $class = shift; my ($name, $value, $globals) = @_; my $tags = \%{"$class\::EXPORT_TAGS"}; return $class->_exporter_merge_opts($value, $globals, $tags->{$name}->($class, @_)) if ref($tags->{$name}) eq q(CODE); return $class->_exporter_merge_opts($value, $globals, @{$tags->{$name}}) if exists $tags->{$name}; return $class->_exporter_merge_opts($value, $globals, @{"$class\::EXPORT"}, @{"$class\::EXPORT_OK"}) if $name eq 'all'; return $class->_exporter_merge_opts($value, $globals, @{"$class\::EXPORT"}) if $name eq 'default'; $globals->{$name} = $value || 1; return; } # Given a regexp-like string, looks it up in @EXPORT_OK and returns the # list of matching functions. # sub _exporter_expand_regexp { no strict qw(refs); our %TRACKED; my $class = shift; my ($name, $value, $globals) = @_; my $compiled = eval("qr$name"); my @possible = $globals->{is_unimport} ? keys( %{$TRACKED{$class}{$globals->{into}}} ) : @{"$class\::EXPORT_OK"}; $class->_exporter_merge_opts($value, $globals, grep /$compiled/, @possible); } # Helper for _exporter_expand_sub. Returns a regexp matching all subs in # the exporter package which are available for export. # sub _exporter_permitted_regexp { no strict qw(refs); my $class = shift; my $re = join "|", map quotemeta, sort { length($b) <=> length($a) or $a cmp $b } @{"$class\::EXPORT"}, @{"$class\::EXPORT_OK"}; qr{^(?:$re)$}ms; } # Given a sub name, returns a hash of subs to install (usually just one sub). # Keys are sub names, values are coderefs. # sub _exporter_expand_sub { my $class = shift; my ($name, $value, $globals, $permitted) = @_; $permitted ||= $class->_exporter_permitted_regexp($globals); no strict qw(refs); if ($name =~ $permitted) { my $generator = $class->can("_generate_$name"); return $name => $class->$generator($name, $value, $globals) if $generator; my $sub = $class->can($name); return $name => $sub if $sub; } $class->_exporter_fail(@_); } # Called by _exporter_expand_sub if it is unable to generate a key-value # pair for a sub. # sub _exporter_fail { my $class = shift; my ($name, $value, $globals) = @_; return if $globals->{is_unimport}; _croak("Could not find sub '%s' exported by %s", $name, $class); } # Actually performs the installation of the sub into the target package. This # also handles renaming the sub. # sub _exporter_install_sub { my $class = shift; my ($name, $value, $globals, $sym) = @_; my $into = $globals->{into}; my $installer = $globals->{installer} || $globals->{exporter}; $name = ref $globals->{as} ? $globals->{as}->($name) : ref $value->{-as} ? $value->{-as}->($name) : exists $value->{-as} ? $value->{-as} : $name; return unless defined $name; unless (ref($name)) { my ($prefix) = grep defined, $value->{-prefix}, $globals->{prefix}, q(); my ($suffix) = grep defined, $value->{-suffix}, $globals->{suffix}, q(); $name = "$prefix$name$suffix"; } return ($$name = $sym) if ref($name) eq q(SCALAR); return ($into->{$name} = $sym) if ref($into) eq q(HASH); no strict qw(refs); if (exists &{"$into\::$name"} and \&{"$into\::$name"} != $sym) { my ($level) = grep defined, $value->{-replace}, $globals->{replace}, q(0); my $action = { carp => \&_carp, 0 => \&_carp, '' => \&_carp, warn => \&_carp, nonfatal => \&_carp, croak => \&_croak, fatal => \&_croak, die => \&_croak, }->{$level} || sub {}; $action->( $action == \&_croak ? "Refusing to overwrite existing sub '%s::%s' with sub '%s' exported by %s" : "Overwriting existing sub '%s::%s' with sub '%s' exported by %s", $into, $name, $_[0], $class, ); } our %TRACKED; $TRACKED{$class}{$into}{$name} = $sym; no warnings qw(prototype); $installer ? $installer->($globals, [$name, $sym]) : (*{"$into\::$name"} = $sym); } sub _exporter_uninstall_sub { our %TRACKED; my $class = shift; my ($name, $value, $globals, $sym) = @_; my $into = $globals->{into}; ref $into and return; no strict qw(refs); # Cowardly refuse to uninstall a sub that differs from the one # we installed! my $our_coderef = $TRACKED{$class}{$into}{$name}; my $cur_coderef = exists(&{"$into\::$name"}) ? \&{"$into\::$name"} : -1; return unless $our_coderef == $cur_coderef; my $stash = \%{"$into\::"}; my $old = delete $stash->{$name}; my $full_name = join('::', $into, $name); foreach my $type (qw(SCALAR HASH ARRAY IO)) # everything but the CODE { next unless defined(*{$old}{$type}); *$full_name = *{$old}{$type}; } delete $TRACKED{$class}{$into}{$name}; } sub mkopt { my $in = shift or return []; my @out; $in = [map(($_ => ref($in->{$_}) ? $in->{$_} : ()), sort keys %$in)] if ref($in) eq q(HASH); for (my $i = 0; $i < @$in; $i++) { my $k = $in->[$i]; my $v; ($i == $#$in) ? ($v = undef) : !defined($in->[$i+1]) ? (++$i, ($v = undef)) : !ref($in->[$i+1]) ? ($v = undef) : ($v = $in->[++$i]); push @out, [ $k => $v ]; } \@out; } sub mkopt_hash { my $in = shift or return; my %out = map +($_->[0] => $_->[1]), @{ mkopt($in) }; \%out; } 1; __END__ =pod =encoding utf-8 =for stopwords frobnicate greps regexps =head1 NAME Exporter::Tiny - an exporter with the features of Sub::Exporter but only core dependencies =head1 SYNOPSIS package MyUtils; use base "Exporter::Tiny"; our @EXPORT = qw(frobnicate); sub frobnicate { ... } 1; package MyScript; use MyUtils "frobnicate" => { -as => "frob" }; print frob(42); exit; =head1 DESCRIPTION Exporter::Tiny supports many of Sub::Exporter's external-facing features including renaming imported functions with the C<< -as >>, C<< -prefix >> and C<< -suffix >> options; explicit destinations with the C<< into >> option; and alternative installers with the C<< installer >> option. But it's written in only about 40% as many lines of code and with zero non-core dependencies. Its internal-facing interface is closer to Exporter.pm, with configuration done through the C<< @EXPORT >>, C<< @EXPORT_OK >> and C<< %EXPORT_TAGS >> package variables. If you are trying to B a module that inherits from Exporter::Tiny, then look at: =over =item * L =item * L =back If you are trying to B a module that inherits from Exporter::Tiny, then look at: =over =item * L =back =head1 BUGS Please report any bugs to L. =head1 SUPPORT B<< IRC: >> support is available through in the I<< #moops >> channel on L. =head1 SEE ALSO Simplified interface to this module: L. Other interesting exporters: L, L. =head1 AUTHOR Toby Inkster Etobyink@cpan.orgE. =head1 COPYRIGHT AND LICENCE This software is copyright (c) 2013-2014, 2017 by Toby Inkster. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =head1 DISCLAIMER OF WARRANTIES THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.