Date Created: 2025-03-30
By: 16BitMiker
[ BACK.. ]
Perl stands out as a language that comfortably supports procedural programming, even as modern trends lean toward object-oriented paradigms. For utility scripts, quick tooling, and modular command-line programs, procedural Perl remains clean, fast, and easy to reason about.
In this post, we’ll walk through how to create and use custom procedural Perl packages in a structured project. We'll highlight a particularly useful trick: how to use FindBin’s $RealBin
to reliably locate your project’s lib/
directory even when running scripts from a nested pl/
directory. We'll also explore Perl’s Exporter
module and how to use tags like :all
to manage what your modules expose.
Let’s assume this simple but common directory structure:
project-root/
├── lib/ # Your custom packages (.pm files)
├── modelfile/ # Optional config or data
├── pl/ # Entry-point scripts
└── test/ # Test scripts
Your scripts live in pl/
and must load modules from the top-level lib/
directory—not from a non-existent pl/lib/
.
When a script like pl/file.pl
tries to load a module from lib/
, Perl needs that path added to @INC
, the array of module search paths. But by default, @INC
won’t include ../lib
unless you explicitly push it.
This is where the FindBin module—and more specifically, $RealBin
—comes to the rescue.
The FindBin module provides the $RealBin
variable, which holds the fully resolved, symlink-free absolute path to the directory where your script is located.
If you execute:
xxxxxxxxxx
perl pl/file.pl
Then $RealBin
will equal something like:
xxxxxxxxxx
/home/user/project-root/pl
If you naively write:
xxxxxxxxxx
use "$RealBin/lib";
Perl will look for modules in:
xxxxxxxxxx
/home/user/project-root/pl/lib
But your actual modules live in:
xxxxxxxxxx
/home/user/project-root/lib
Here’s a clean one-liner that fixes the path:
xxxxxxxxxx
use ($RealBin, "$RealBin/lib" =~ s~pl/~~ );
🔍 Breakdown:
$RealBin
is /home/user/project-root/pl
"$RealBin/lib"
becomes /home/user/project-root/pl/lib
=~ s~pl/~~r
strips out the pl/
segment
The r
flag means "return the modified string"
Result: /home/user/project-root/lib
This trick ensures both the current script directory and the top-level lib/
directory are included in @INC
—without hardcoding relative paths.
Let’s build a simple package in lib/MikerGeneral.pm
:
xpackage ;
use ;
use ;
use qw(import);
use qw(say);
use :: qw(:constants);
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Export
# Declare what can be exported
our @EXPORT_OK = qw(msg border clear);
# Define export tags for convenience
our %EXPORT_TAGS = (
=> [@EXPORT_OK], # :all tag exports everything
=> [qw(border clear)],
);
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Functions
# Print a green message label and text
sub {
my ($text) = @_;
say , , "[msg] ", , $text;
}
# Print a colored border with optional label
sub {
my ($label) = @_;
say , , "-" x 40, ;
say , , $label, if defined $label;
say , , "-" x 40, ;
}
# Clear the terminal screen
sub {
print "\e[2J\e[H";
}
1; # Required for all .pm files
📝 Key Points:
We use Exporter to selectively share functions
@EXPORT_OK
lists functions that are available to import
%EXPORT_TAGS
groups exports under named tags like :all
or :visual
Here’s how you’d use this module inside pl/file.pl
:
xxxxxxxxxx
#!/usr/bin/env perl
use ;
use ;
use qw($RealBin);
use qw(say);
# Add pl/ and top-level lib/ to @INC
use ($RealBin, "$RealBin/lib" =~ s~pl/~~ );
# Import selected functions from MikerGeneral
use qw(:all);
say "Running from: $RealBin";
# Use functions from the custom package
"Hello from Procedural Perl"); (
"Ready to go!"); (
📌 Notes:
use lib
ensures Perl can find your modules
use MikerGeneral qw(:all);
imports all functions tagged under :all
You could also do use MikerGeneral qw(msg clear);
for selective imports
From your project root, run:
xxxxxxxxxx
perl pl/file.pl
You should see:
xxxxxxxxxx
Running from: /absolute/path/to/project-root/pl
----------------------------------------
Hello from Procedural Perl
----------------------------------------
[msg] Ready to go!
✅ And no errors about missing modules.
Using Exporter effectively helps keep your namespace clean and your modules flexible.
Element | Purpose |
---|---|
@EXPORT_OK | List of functions or variables that can be imported |
%EXPORT_TAGS | Named groups of exports (e.g., :all , :visual ) |
use Module qw(:tag) | Import all items under a tag defined in %EXPORT_TAGS |
use Module qw(func1 func2) | Import only the listed functions |
Avoid using @EXPORT
unless you want to import symbols by default (not recommended for procedural libraries).
Here’s what we learned:
✅ Using FindBin’s $RealBin
with a regex lets you dynamically point to your top-level lib/
directory
✅ Procedural packages are easy to build using Exporter
✅ Tags like :all
and :visual
make your module’s interface flexible and clean
✅ This setup keeps your scripts portable, maintainable, and organized
With just a few lines of thoughtful path logic and a clean module structure, you can build powerful procedural Perl systems that scale without the baggage of full-blown object-oriented design. Happy hacking! 🐪💻