Home / Alt manpages / perlmod(1)

  • perlmod(1)
  • User command
  • linux

Build a Small Perl Module with Private State and Explicit Exports

A Perl module that exports too much, or hides state in the wrong place, is a headache six months later when someone else calls it. This walks through a tiny module that keeps its internal state private and only exports what it means to. Examples target Perl 5.38.2, as documented by the installed perlmod page from the perl-doc package.

Allow about twenty minutes. You need Perl and a shell, everything runs as your normal user in a temporary directory, so nothing here needs elevated privileges. The module is intentionally small: the lesson is where names live, when code loads, and what an export actually 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 behaviour below is based on 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 your suffix will differ. Keep the shell open until the checks finish.

Recovery

Once you are done, 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 the file. It does not move lexical variables declared with my.
  • $prefix is therefore private to this module file.
  • greet is made available for optional import through @EXPORT_OK, and nothing else is exposed.
  • 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

Both 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 at all.

Checkpoint

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, the documented 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 at all.

Warning

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. Watch load time versus run time

use Module is processed in a BEGIN phase: 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. That is why declarations or imports made by use are already available to later source in the same file.

Tip

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, which makes it a strange place to hide something noisy.

7. Test the failure boundary

Requesting a name that is not in @EXPORT_OK should fail during compilation. That 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

  • Confirmed the local Perl and perl-doc versions.
  • Loaded Greeting.pm from an explicit local search path.
  • Called greet both through an approved import and a qualified call.
  • Kept the private lexical distinct from the package namespace.
  • Watched use load and import during compilation.
  • Confirmed an unapproved import fails, and removed the temporary workspace only when safe.