Build a Small Perl Module with a Private Package and Explicit Exports
You will finish with a working Perl module whose package name is separate from the caller, whose internal state stays private, and whose public function is exported deliberately. The examples target Perl 5.38.2, as documented by the installed perlmod page from the perl-doc package.
The route
Jump straight to the step you need, or tick off Done means at the end.
- 1. Check the installed Perl
- 2. Create an isolated module directory
- 3. Write a module with one public function
- 4. Load the module and import only what you asked for
- 5. See the difference between package and lexical scope
- 6. Observe load time without confusing it with run time
- 7. Test the failure boundary
- 8. Remove the temporary files
Allow about twenty minutes. You need Perl and a shell. Everything runs as your normal user in a temporary directory, so no elevated privileges are required. The module is intentionally small: the useful lesson is where names live, when code loads, and what an export changes.
1. Check the installed Perl
Confirm the interpreter before relying on version-specific diagnostics:
$ perl -e 'printf "%vd\n", $^V'
v5.38.2
$ dpkg-query -W -f='${Package} ${Version}\n' perl perl-doc
perl 5.38.2-3.2ubuntu0.6
perl-doc 5.38.2-3.2ubuntu0.6
Your package revision may differ. The language behaviour below is based on Perl 5.38.2. Checkpoint: if perl is missing, stop here and use the normal package-management process for your distribution. Do not use sudo merely to run these examples.
2. Create an isolated module directory
Make a temporary workspace and enter it. This changes only a directory below /tmp:
$ workdir=$(mktemp -d /tmp/perlmod-demo.XXXXXX)
$ cd "$workdir"
$ printf 'workspace: %s\n' "$PWD"
workspace: /tmp/perlmod-demo.XXXXXX
The final directory name is generated by mktemp, so the displayed suffix will differ. Keep the shell open until the checks finish. Recovery is simple: after checking the results, leave the directory and remove that exact path with rm -rf -- "$workdir". That deletion is irreversible, so do not run it until you no longer need the files.
3. Write a module with one public function
Create Greeting.pm with a package declaration, a private lexical, an explicit export list, and a function:
$ cat > Greeting.pm <<'PERL'
package Greeting;
use strict;
use warnings;
use Exporter qw(import);
our @EXPORT_OK = qw(greet);
my $prefix = 'Hello';
sub greet {
my ($name) = @_;
return "$prefix, $name";
}
1;
PERL
package Greeting puts unqualified dynamic names and subroutine names in the Greeting namespace for the rest of this file. It does not move lexical variables declared with my. The $prefix value is therefore private to this module file, while greet is made available for optional import through @EXPORT_OK.
The final 1; matters: a module loaded with use or require must return a true value. The module does not need root access, and it does not install anything system-wide.
4. Load the module and import only what you asked for
Write a caller that adds the current directory to Perl's module search path and requests greet explicitly:
$ cat > demo.pl <<'PERL'
use strict;
use warnings;
use lib '.';
use Greeting qw(greet);
print greet('Ada'), "\n";
print Greeting::greet('Grace'), "\n";
PERL
$ perl demo.pl
Hello, Ada
Hello, Grace
The two calls reach the same module function. The first uses the imported short name; the second uses its fully qualified package name. Prefer the qualified form when it makes ownership clearer or when you do not want to import a name into the caller.
Checkpoint: run the caller with warnings enabled as shown. A clean run prints two lines and no diagnostic. If Perl says it cannot locate Greeting.pm, check that you are still in the temporary directory and that use lib '.' appears before use Greeting.
5. See the difference between package and lexical scope
Change the caller to inspect the public function without reaching into the private lexical:
$ cat > scope-check.pl <<'PERL'
use strict;
use warnings;
no warnings 'once';
use lib '.';
use Greeting ();
print Greeting::greet('Linus'), "\n";
print defined $Greeting::prefix ? "package variable\n" : "no package variable\n";
PERL
$ perl scope-check.pl
Hello, Linus
no package variable
use Greeting () loads the module without importing names. In the installed perlmod documentation, this is the explicit form for making a module available while skipping its import step. The module's my $prefix is lexical, so $Greeting::prefix is not the same value and is not defined by this module.
Do not edit a module's symbol table directly to discover or create state. The manual warns that direct changes to entries which are not already typeglobs have undefined results that can change between Perl releases.
6. Observe load time without confusing it with run time
use Module is processed in a BEGIN phase. It is broadly equivalent to requiring the module and then calling its import method during compilation. Add a small compile-time message to a separate module to make that ordering visible:
$ cat > Phase.pm <<'PERL'
package Phase;
use strict;
use warnings;
BEGIN { print "module compiled\n"; }
sub run { print "subroutine ran\n"; }
1;
PERL
$ perl -I. -e 'use Phase; print "main ran\n"; Phase::run()'
module compiled
main ran
subroutine ran
The BEGIN block runs as soon as it has been compiled, before the rest of the one-line program executes. This is why declarations or imports made by use are available to later source in the same file. Keep module loading free of surprising side effects: a diagnostic, network connection or file change in BEGIN happens before ordinary run-time control reaches the caller.
7. Test the failure boundary
Requesting a name that is not in @EXPORT_OK should fail during compilation. This is a useful check that your public interface is intentional:
$ perl -I. -MGreeting=not_public -e 'print not_public()'
Undefined subroutine &main::not_public called at -e line 1.
$ printf 'exit status: %s\n' "$?"
exit status: 255
The exact diagnostic location can vary, but the command must fail with a non-zero status. Do not solve this by exporting every internal helper. Add a name to @EXPORT_OK only when callers should rely on it, then document or test the resulting interface.
8. Remove the temporary files
When the checks are complete, clean up the exact temporary workspace created earlier:
$ cd /tmp
$ rm -rf -- "$workdir"
$ test ! -e "$workdir" && echo 'temporary workspace removed'
temporary workspace removed
If you need to inspect a failed run, leave the directory in place instead and remove it later after recording the error. None of the examples altered Perl's installed modules, your shell configuration or a service.
Done means
- You confirmed the local Perl and
perl-docversions. Greeting.pmloaded from an explicit local search path.greetworked both through an approved import and a qualified call.- The private lexical remained distinct from the package namespace.
- You saw that
useloads and imports during compilation. - An unapproved import failed, and the temporary workspace was removed only when safe.