This follow-up blog post shares the improvements to my earlier zero-dependency Role implementation in Perl. Follow the link below for more details: https://theweeklychallenge.org/blog/class-and-role
Add isIDCONT_lazy_if_safe() Various places in the code are using isWORDCHAR to match the continuation in an identifier. This mostly works, but the two sets are not identical, and the proper thing to do is to match continuation characters. The infrastructure was lacking this macro that would make it easy to do the right thing. This commit adds the infrastructure, leaving it to future commits to use it. A reasonably complete list of characters that differ between the two sets is: MIDDLE DOT GREEK YPOGEGRAMMENI GREEK ANO TELEIA COMBINING CYRILLIC HUNDRED THOUSANDS SIGN COMBINING CYRILLIC MILLIONS SIGN ARMENIAN MODIFIER LETTER LEFT HALF RING ARMENIAN EMPHASIS MARK NEW TAI LUE THAM DIGIT ONE COMBINING PARENTHESES OVERLAY COMBINING ENCLOSING CIRCLE COMBINING ENCLOSING CIRCLE BACKSLASH COMBINING ENCLOSING SCREEN COMBINING ENCLOSING UPWARD POINTING TRIANGLE MANDAIC LETTER AZ ESTIMATED SYMBOL CIRCLED LATIN CAPITAL LETTER A ... CIRCLED LATIN SMALL LETTER Z VERTICAL TILDE KATAKANA MIDDLE DOT COMBINING CYRILLIC TEN MILLIONS SIGN COMBINING CYRILLIC THOUSAND MILLIONS SIGN ARABIC LIGATURE SHADDA WITH DAMMATAN ISOLATED FORM ARABIC LIGATURE SHADDA WITH SUPERSCRIPT ALEF ISOLATED FORM ARABIC LIGATURE SALLALLAHOU ALAYHE WASALLAM ARABIC LIGATURE JALLAJALALOUHOU ARABIC FATHATAN ISOLATED FORM ARABIC DAMMATAN ISOLATED FORM ARABIC KASRATAN ISOLATED FORM ARABIC FATHA ISOLATED FORM ARABIC DAMMA ISOLATED FORM ARABIC KASRA ISOLATED FORM ARABIC SHADDA ISOLATED FORM ARABIC SUKUN ISOLATED FORM HALFWIDTH KATAKANA MIDDLE DOT SQUARED LATIN CAPITAL LETTER A SQUARED LATIN CAPITAL LETTER Z NEGATIVE CIRCLED LATIN CAPITAL LETTER A ... NEGATIVE CIRCLED LATIN CAPITAL LETTER Z NEGATIVE SQUARED LATIN CAPITAL LETTER A ... NEGATIVE SQUARED LATIN CAPITAL LETTER Z
My task was to port debian12 to debian13. In debian13 I noticed that the website was behaving differently with Perl v5.40.1. I found out that the following line returns different values on different Perl versions.
my $IS_NGINX=(`pgrep lighttpd` ? 0:1);
The value should be 0 if lighttpd is running, otherwise 1 for NGINX. In debian11 and debian12 the result is correct. But in debian13 the value is 1 though lighttpd is running. No errors or warnings are thrown. use strict
is set in every perl module.
I replaced the status request with
my $IS_NGINX =(`systemctl status lighttpd > /dev/null; echo $?`);
if ( $IS_NGINX == 0){
$IS_NGINX = 0;
}
else {
$IS_NGINX = 1;
and it seems to work on debian13 too. But I would still like to know why the prior line leads to a different result with the new Perl version, especially since no warnings are given.
Edit: Analyzing the problem further it seems that the problem appears if I use the ternary operator. And it does not matter if pgrep
or systemctl status
is used.
Edit: after days of searching I found out that the reason pgrep is not working in the code like it used to do is because with the new Debian version there are also new settings in systemd service for the web server. In particular ProtectProc
was set to invisible
hints/darwin.sh: Provide reference for poll issue From OpenSSH 8.9 release notes: For character-special devices like /dev/null, Darwin's poll(2) returns POLLNVAL when polled with POLLIN. Apparently this is Apple bug 3710161 - not public but a websearch will find other OSS projects rediscovering it periodically since it was first identified in 2005. https://www.openssh.com/txt/release-8.9
INSTALL: Add a bunch of pod markup There's more that could be added; but this is a lot
perlapi: Some elements are documented in INSTALL This adds =for apidoc lines to INSTALL for use by autodoc.pl
INSTALL: Replace tabs with blanks
![]() | For the second consecutive year, The Perl and Raku Foundation (TPRF) is overjoyed to announce a donation of USD 25,000 from DuckDuckGo. This is our largest gift of 2025 and it will advance some very important work in the Perl 5 core. I'd like to thank Joseph Jerome, Oriol Soriano and Gabriel Weinberg for their support in making this happen. It's a big deal for us. ♥️ More discussion at https://news.ycombinator.com/item?id=45439883 [link] [comments] |
The Experiment
At the beginning of the year, we ran a small experiment at work. We hired four annotators and let them rate 900 sentences (the details are not important). To decide whether the inter-annotator agreement was significant, we calculated (among others) Krippendorff’s alpha coefficient.
I’d used Perl for everything else in the project, so I reached for Perl to calculate the alpha, as well. I hadn’t found any module for it on CPAN, so I wrote one: I read the Wikipedia page and implemented the formulas.
The Real Data
The experiment was promising, so we got additional funding. We hired 3 more annotators, and a few months later, another nine. This increased the number of raters to 16. So far, they’ve rated about 200K sentences. Each sentence has been annotated by at least two annotators (usually three).
One day, I decided to calculate the inter-annotator agreement for the new data. To my surprise, the calculation took more than 6 hours.
Profiling
I ran the NYTProf profiler on a smaller dataset to discover the problematic areas of the code. Browsing the results I clearly identified the culprit: the method responsible for building the coincidence matrix.
sub _build_coincidence($self) {
my %coinc;
my @s = $self->vals;
for my $v (@s) {
for my $v_ (@s) {
$coinc{$v}{$v_} = sum(map {
my $unit = $_;
my @k = keys %$unit;
sum(0,
map {
my $i = $_;
scalar grep $unit->{$_} eq $v_,
grep $i ne $_, @k
} grep $unit->{$_} eq $v, @k
) / (@k - 1)
} @{ $self->units });
}
}
return \%coinc
}
You can see the four nested loops here (two for
’s and two map
’s). I apologise, that’s how I’d understood the formula.
The Solution Suggested by AI
“This is the right time to experiment with AI!” thought I to myself. I started a session with GPT-4o, showed it the whole module and asked for a more efficient version. The LLM correctly identified the four nested loops, suggested an improvement, and generated code to implement it.
Unfortunately, the code didn’t even compile. I asked the AI several times to fix the problems, until I got a runnable code, but its running time wasn’t different to the original implementation. So I asked it to try harder, and after a third iteration of the whole ordeal, we had the following code that ran for 20 seconds instead of 22:
sub _build_coincidence($self) {
my %coinc;
my @vals = $self->vals;
# Initialize coincidence counts
for my $v (@vals) {
for my $v_ (@vals) {
$coinc{$v}{$v_} = 0;
}
}
# Iterate over each unit
for my $unit (@{ $self->units }) {
my @keys = keys %$unit;
my $unit_count = @keys;
# Count occurrences of each value in the current unit
my %value_count;
$value_count{ $unit->{$_} }++ for @keys;
# Calculate coincidences based on the value counts
for my $v (@vals) {
for my $v_ (@vals) {
if (exists $value_count{$v} && exists $value_count{$v_}) {
my $coinc_count = 0;
# Count pairs of keys that match the values
for my $key1 (@keys) {
for my $key2 (@keys) {
next if $key1 eq $key2; # Skip same keys
if ($unit->{$key1} eq $v && $unit->{$key2} eq $v_) {
$coinc_count++;
}
}
}
# Update the coincidence count
$coinc{$v}{$v_} += $coinc_count / ($unit_count - 1) if $unit_count > 1;
}
}
}
}
return \%coinc
}
That’s many lines. We’ve got comments, which should be nice, but… For the large data, I’d still have had to wait for several hours.
The Optimisation
I decided to bite the bullet and tried to optimise the code myself. I went over both the original code and the suggested improvement to realise the LLM wanted to cache the number of values in %value_count
but never really used the value: it only checked for existence.
What was really needed was not to iterate over all the values every time skipping the irrelevant ones, but to precompute the relevant values and reduce the breadth of the loops. Usually, there were only two or three values per unit (each sentence was annotated by at least two annotators), so there was no reason to check all sixteen possible.
This is the code I wrote and released as the new version of the module. For the large dataset, the running time dropped from six hours to four seconds.
sub _build_coincidence($self) {
my @vals = $self->vals;
my %coinc;
@{ $coinc{$_} }{@vals} = (0) x @vals for @vals;
for my $unit (@{ $self->units }) {
my %is_value;
@is_value{ values %$unit } = ();
my @values = keys %is_value;
my @keys = keys %$unit;
for my $v (@values) {
for my $v_ (@values) {
my $coinc_count = 0;
for my $key1 (@keys) {
for my $key2 (@keys) {
next if $key1 eq $key2;
++$coinc_count
if $unit->{$key1} eq $v
&& $unit->{$key2} eq $v_;
}
}
$coinc{$v}{$v_} += $coinc_count / (@keys - 1);
}
}
}
return \%coinc
}
I showed my final code to the LLM. It congratulated me on the brilliant solution, emphasising some of the tricks I’d used to make it faster (e.g. filling the matrix by zeros using the repetition operator).
Did it help me? I’m not sure. It motivated me to study the code and find the solution, but it didn’t really contribute to it. It took me about two hours including finding the solution myself.
What’s your experience with AI?
For the second consecutive year, The Perl and Raku Foundation (TPRF) is overjoyed to announce a donation of USD 25,000 from DuckDuckGo.
DuckDuckGo has demonstrated how Perl and its ecosystem can deliver power and scale to drive the DuckDuckGo core systems, plug-in framework and Instant Answers. The Foundation is grateful that DuckDuckGo recognises the importance of Perl, and for their generous funding support for a second year through their charitable donations programme.
– Stuart J Mackintosh, President of The Perl and Raku Foundation
Last year’s donation of USD 25,000 from DuckDuckGo was instrumental in helping to fund the foundation’s Core Perl Maintenance Fund and this year’s donation will help to fund more of the same crucial work that keeps the Perl language moving forward.
Paul “LeoNerd” Evans is one of the developers who gets regular funding from the Perl Core Maintenance Fund. Here is a short list of just some of the many contributions which Paul has made to core Perl as part of the maintenance fund work:
- The builtin module (5.36), making available many new useful language-level utilities that were previously loaded from modules like Scalar::Util
- The complete feature ‘class’ system (5.38), adding proper object-orientation syntax and abilities
- Lexical method support (5.42), adding
my method
and the$obj->&method
invocation syntax for better object encapsulation - Stabilising some of the recent experiments - signatures (5.36), try/catch (5.40), foreach on multiple vars (5.40)
- Ability to use the //= and ||= operators in signatures (5.38), performance improvements and named parameters (upcoming in next release)
- The new any and all keywords (5.42)
We look forward to many more innovative contributions from Paul over the coming year.
While TPRF never takes continued support for granted, when it does arrive, it allows the foundation to plan for the future with much greater confidence. Multi-year partnerships with our sponsors allow us to continue to prioritize important work, knowing that we will have the runway that we need to fund the work which helps to sustain the Perl Language and its associated communities.
For more information on how to become a sponsor, please contact: olaf@perlfoundation.org
"Fireworks" by colink. is licensed under CC BY-SA 2.0.
![]() | I think that many people aren't aware of the existence of Planet Perl (it gets 4-5 visitors on most days). Anyway. It exists. And I've just given it a bit of a redesign. You may find it useful and/or interesting. [link] [comments] |
This package of the Perl Data Language https://metacpan.org/pod/PDL::Minuit
When I run only the parameter definition part of the example program:
use PDL::LiteF;
use PDL::Minuit;
mn_init(\&$chi2, {Title => 'test title'});
mn_def_pars($pars=pdl(2.5,3.0), $steps=pdl(0.3,0.5), {Names => [qw(intercept slope)]});
I get the following output:
minuit release 90.10 initialized. dimensions 100/ 50 epsmac= 0.89E-15
parameter definitions:
no. name value step size limits
1 'intercept ' 2.5000 0.30000 no limits
2 'slope ' 2.5000 0.30000 no limits
The error is that the value of the initialization parameters is 2.5 and 3.0, not 2.5 and 2.5.
PDL::Minuit
seems to repeat the first coordinate of the initialization vector in all coordinates.
Am I missing something? In case I am not, I think one could hack this problem by re-scaling all variables, but I'd prefer to fix the code. Do you see where is the problem with the module code?
We finally managed to arrange our first regular meeting between the three of us.
- Largely we discussed strategy for the named parameters branch. We agreed to merge soon (so people start playing with it), just staying ready to back it out well before release, in case it proves not to be ready.
- In that context, we considered the situation with experiment warnings. We agreed that named parameters should be an additional experiment of its own, though it is an adjunct of another experiment – and we do not yet have established ways of dealing with such a situation. We want to think about how our experiment mechanism should be extended to cover such cases.
Ongoing scheduling issues have meant we haven’t met all three together for a while, but today Paul and Leon found a time to discuss a few issues.
Dev point releases
5.43.3 just went out. .4 to .7 are accounted for, so we’ve a few months yet on that. Nothing for PSC to do for now.
OpenSSL progress
Leon has been building a replacement for Net::SSLeay
- an XS wrapper
of libssl
. Eventual plan is that IO::Socket::SSL
should be able to
use that instead. Eventual intention is that it can be bundled with the
actual core perl
dist and get us ability to use https
URLs from the
in-core CPAN client directly.
We should have a technical meeting at some point to round up some of the interested parties. PSC’s involvement can just be keeping an eye on it, and seeing if it is on track to be included in (5.)44.
TODO: Plan a time and audience for said meeting.
Better categorization of core vs dual-life modules
There’s probably more modules in ext/
than there ought to be (e.g.
File-Find
). It’d be nice to tidy these up and move them out into
cpan/
or dist/
.
TODO: Leon will email p5p@ to form a list of suggestions
In-core module modernizing
Started by JRaspass, we have been modernising some core modules and
unit tests, by ensuring they begin use v5.40;
This has mostly gone
fine, and is a thing we wish to encourage and invite more people to do
with more files.
Paul encountered a problem with attributes.pm
(well, .xs) wherein it
picks up a warning from the wrong caller layer. It’s a bug that can be
fixed but it might provoke illegalproto warnings in code that
previously didn’t. But this is probably fine in the general category of
“eh, new perls might warn about new stuff” so probably fine.
We’ll carry on doing more module modernisations, but we should wait on the results of better categorization (see above) first.
“Signatures Named Parameters”
Paul wants to merge the signatures-named-parameters
branch sometime
soon, because it’s been sat for 2 months with no comments. Will set a
“will merge on this date” deadline - comments would be most appreciated
by then.
One remaining thought is whether this new ability should provoke expermental warnings? If so, in what category?
In general we don’t actually have a policy, or any guiding historic precedent, on how to continue an experiment once it (or parts of it) is declared stable.
TODO: Ask p5p@ what we think of this expermenting plan.
“English Names”
There’s been two previous suggested implementors for this, but no code has ended up materialising. I (Paul) will probably just have a go at writing it myself so then it will be done. It would have been a useful mentoring/teaching opportunity, but since we’ve already lost two people by attempting it, I don’t want to risk a third ;)
Let's say I want to replace every occurrence of 12345
in file1
with the contents of file2
. The following code works:
use strict;
use warnings;
local $/;
open my $fh, 'file1' or die "$!";
my $file1 = <$fh>;
close $fh;
open $fh, 'file2' or die "$!";
my $file2 = <$fh>;
close $fh;
my $file3 = $file1 =~ s/12345/$file2/gr;
open $fh, '>>', 'file3' or die "$!";
print $fh $file3;
close $fh;
I'm probably overthinking this but from a theoretical standpoint what would be the proper, efficient way to do this without causing a lot of copying in memory? Also, I'm new to Perl so feel free to point out anything here that goes against Perl best practices.
![]() | submitted by /u/briandfoy [link] [comments] |
Weekly Challenge 340
Each week Mohammad S. Anwar sends out The Weekly Challenge, a chance for all of us to come up with solutions to two weekly tasks. My solutions are written in Python first, and then converted to Perl. It's a great way for us all to practice some coding.
Task 1: Duplicate Removals
Task
You are given a string, $str
, consisting of lowercase English letters.
Write a script to return the final string after all duplicate removals have been made. Repeat duplicate removals on the given string until we no longer can.
A duplicate removal consists of choosing two adjacent and equal letters and removing them.
My solution
Thanks to regular expressions this is relatively straight forward. We can use the expression (.)\1
to get two of the same character. .
will match any single letter, ()
will capture it, and \1
will use the capture.
For the Python solution, I run the regexp on the input_string
and store it in a variable called new_string
. If it is the same, I return it. If it is different (because we removed two characters), I reset input_string
and call the loop again.
def duplicate_removals(input_string: str) -> str:
while True:
new_string = re.sub(r'(.)\1', '', input_string)
if new_string == input_string:
return new_string
input_string = new_string
In Perl, calling the s
function returns the number of substitutions made. Therefore the Perl solution uses this feature rather than using the new_string
variable. The loop will exit when no substitutions are made.
sub main ($input_string) {
my $changes = 1;
while ($changes) {
$changes = ($input_string =~ s/(.)\1+//g);
}
say "'$input_string'";
}
Examples
$ ./ch-1.py abbaca
'ca'
$ ./ch-1.py azxxzy
'ay'
$ ./ch-1.py aaaaaaaa
''
$ ./ch-1.py aabccba
'a'
$ ./ch-1.py abcddcba
''
Task 2: Ascending Numbers
Task
You are given a string, $str
, is a list of tokens separated by a single space. Every token is either a positive number consisting of digits 0-9 with no leading zeros, or a word consisting of lowercase English letters.
Write a script to check if all the numbers in the given string are strictly increasing from left to right.
My solution
The first thing I do is extract the numbers from input_string
(str
is a reserved word in Python). For this I use list comprehension, along with a regular expression to return words that contain digits only.
def ascending_numbers(input_string: str) -> bool:
number_list = [int(n) for n in input_string.split() if re.match(r'^\d+$', n)]
Perl doesn't have list comprehension, but the same functionality can be achieved by using the grep
function.
sub main ($input_string) {
my @number_array = grep { /^\d+$/ } split /\s+/, $input_string;
It's then a matter of checking that the list (array in Perl) is sorted correctly. For this I have a loop with the variable i
. It starts at 1
, to one less than the length of the number_list
list. For each iteration, I check that the number at the position i-1
is less than the number at position i
. If it isn't, I return False
. If the loop is exhausted, I return True
.
for i in range(1, len(number_list)):
if number_list[i-1] >= number_list[i]:
return False
return True
The Perl code follows a similar pattern.
foreach my $i ( 1 .. $#number_array ) {
if ( $number_array[ $i - 1 ] >= $number_array[$i] ) {
say "false";
return;
}
}
say "true";
Examples
$ ./ch-2.py "The cat has 3 kittens 7 toys 10 beds"
True
$ ./ch-2.py 'Alice bought 5 apples 2 oranges 9 bananas'
False
$ ./ch-2.py 'I ran 1 mile 2 days 3 weeks 4 months'
True
$ ./ch-2.py 'Bob has 10 cars 10 bikes'
False
$ ./ch-2.py 'Zero is 0 one is 1 two is 2'
True
-
App::DBBrowser - Browse SQLite/MySQL/PostgreSQL databases and their tables interactively.
- Version: 2.432 on 2025-09-26, with 17 votes
- Previous CPAN version: 2.431 was 3 months, 19 days before
- Author: KUERBIS
-
App::Netdisco - An open source web-based network management tool.
- Version: 2.090002 on 2025-09-25, with 755 votes
- Previous CPAN version: 2.090001
- Author: OLIVER
-
Furl - Lightning-fast URL fetcher
- Version: 3.15 on 2025-09-25, with 105 votes
- Previous CPAN version: 3.14 was 4 years, 4 months, 11 days before
- Author: SYOHEX
-
Module::CoreList - what modules shipped with versions of perl
- Version: 5.20250923 on 2025-09-23, with 44 votes
- Previous CPAN version: 5.20250820 was 1 month, 2 days before
- Author: BINGOS
-
SPVM - The SPVM Language
- Version: 0.990092 on 2025-09-26, with 36 votes
- Previous CPAN version: 0.990091
- Author: KIMOTO
-
Struct::Dumb - make simple lightweight record-like structures
- Version: 0.16 on 2025-09-27, with 16 votes
- Previous CPAN version: 0.15 was 1 month, 7 days before
- Author: PEVANS
-
Syntax::Keyword::Try - a try/catch/finally syntax for perl
- Version: 0.31 on 2025-09-23, with 45 votes
- Previous CPAN version: 0.30 was 1 year, 24 days before
- Author: PEVANS
-
Term::Choose - Choose items from a list interactively.
- Version: 1.776 on 2025-09-26, with 15 votes
- Previous CPAN version: 1.775 was 3 months, 14 days before
- Author: KUERBIS
Roles in Perl, implemented in native form with zero dependencies. Check out where the discussion ends up in the post below:
https://theweeklychallenge.org/blog/roles-in-perl
I am trying to implement a very simple programming language for practice. I'm getting the 'use of uninitialized value in string eq' error while everything is looking good.
the code is attached here.Error is at line 335 and 333. please help me write a working code.
use v5.38;
use experimentals;
class MakeChars {
field $pp = {};
method makeChars($program) {
my @chars = split("", $program);
$pp->{"chars"} = \@chars;
$pp->{"charsLength"} = $#chars;
}
method programLength() {
return $pp->{"charsLength"};
}
method getChar() {
my @chars = @{$pp->{"chars"}};
my $char = shift(@chars);
$pp->{"chars"} = \@chars;
$pp->{"charseLength"} = $#chars;
return $char;
}
method nextChar() {
my @chars = @{$pp->{"chars"}};
return $chars[0];
}
method putChar($char) {
my @chars = @{$pp->{"chars"}};
unshift(@chars, $char);
$pp->{"chars"} = \@chars;
$pp->{"charsLength"} = $#chars;
}
}
class CharGroups {
method isSpaceNewLine($char) {
my @spaceNewLline = (" ", "\n", "\t", "\r");
my %hash = map {$_ => 1} @spaceNewLline;
if(exists($hash{$char})) {return 1;}
return 0;
}
method isDigit($char) {
my @digits = ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
foreach my $digit (@digits) {
if ( $char eq $digit ) {
return 1;
}
}
return 0;
}
method isAlpha($char) {
my @alpha = ();
for my $char ( 'a' ... 'z' ) {
push @alpha, $char;
}
for my $char ( 'A' ... 'Z' ) {
push @alpha, $char;
}
push @alpha, "_";
my %hash = map {$_ => 1} @alpha;
if(exists($hash{$char})) {return 1;}
return 0;
}
method isQuote($char) {
if ( $char eq '"' ) {
return 1;
}
else {
return 0;
}
}
method isSpecialCharachter($char) {
my @specialCharachters = ( "{", "}", "[", "]", ",", ":", "(", ")", ";", "=", "." );
my %hash = map {$_ => 1} @specialCharachters;
if(exists($hash{$char})) {return 1;}
return 0;
}
method isOperator($char) {
my @operators = ( "+", "-", "|", "*", "/", ">", "<", "!", "&", "%" );
my %hash = map {$_ => 1} @operators;
if(exists($hash{$char})) {return 1;}
return 0;
}
}
class Lexer {
method lexer($program) {
my $makeChars = MakeChars->new();
my $CharGroups = CharGroups->new();
my @tokens;
$makeChars->makeChars($program);
my $counter = 0;
my $programLength = $makeChars->programLength();
while($counter <= $programLength) {
my $currentChar = $makeChars->getChar();
$counter++;
if($CharGroups->isSpaceNewLine($currentChar)) { next; }
if($currentChar eq "=" && $makeChars->nextChar() eq "=" ) {
$makeChars->getChar();
$counter++;
my $token = {"type" => "Equals", "value" => "=="};
push(@tokens, $token);
next;
}
if($currentChar eq "." && $makeChars->nextChar() eq "." ) {
$makeChars->getChar();
$counter++;
my $token = {"type" => "Equals", "value" => ".."};
push(@tokens, $token);
next;
}
if ( $CharGroups->isOperator($currentChar) ) {
if ( $currentChar eq "&" ) {
my $nextChar = $makeChars->nextChar();
if ( $nextChar eq "&" ) {
$makeChars->getChar();
$counter++;
my $token = { "type" => "Operator", "value" => "&&" };
push( @tokens, $token );
next;
}
}
}
if ( $CharGroups->isOperator($currentChar) ) {
if ( $currentChar eq "|" ) {
my $nextChar = $makeChars->nextChar();
if ( $nextChar eq "|" ) {
$makeChars->getChar();
$counter++;
my $token = { "type" => "Operator", "value" => "||" };
push( @tokens, $token );
next;
}
}
}
if ( $CharGroups->isOperator($currentChar) ) {
my $token = { "type" => "Operator", "value" => $currentChar };
push( @tokens, $token );
next;
}
if ( $CharGroups->isQuote($currentChar) ) {
my $string = "";
my $delimiter = $currentChar;
$currentChar = $makeChars->getChar();
$counter++;
while ( $currentChar ne $delimiter ) {
$string .= $currentChar;
$currentChar = $makeChars->getChar();
$counter++;
}
my $token = { "type" => "String", "value" => $string };
push( @tokens, $token );
next;
}
if ( $CharGroups->isSpecialCharachter($currentChar) ) {
my $token =
{ "type" => "SpecialCharachter", "value" => $currentChar };
push( @tokens, $token );
next;
}
if ( $CharGroups->isAlpha($currentChar) ) {
my $symbol = "";
$symbol .= $currentChar;
$currentChar = $makeChars->getChar();
$counter++;
while ( $CharGroups->isAlpha($currentChar) ) {
$symbol .= $currentChar;
$currentChar = $makeChars->getChar();
$counter++;
}
$makeChars->putChar($currentChar);
$counter = $counter - 1;
my $token = { "type" => "Symbol", "value" => $symbol };
push( @tokens, $token );
next;
}
if ( $CharGroups->isDigit($currentChar) ) {
my $number = "";
$number .= $currentChar;
$currentChar = $makeChars->getChar();
$counter++;
while ( $CharGroups->isDigit($currentChar) || $currentChar eq "." ) {
$number .= $currentChar;
$currentChar = $makeChars->getChar();
$counter++;
}
$makeChars->putChar($currentChar);
$counter = $counter - 1;
my $token = { "type" => "Number", "value" => $number };
push( @tokens, $token );
next;
}
}
return @tokens;
}
}
class ParseHelpers {
field $pp = {};
method makeTokens(@tokens) {
$pp->{"tokens"} = \@tokens;
$pp->{"tokensLength"} = $#tokens;
}
method tokensLength() {
return $pp->{"tokensLength"};
}
method getToken() {
my @tokens = @{$pp->{"tokens"}};
my $currentToken = shift(@tokens);
$pp->{"tokens"} = \@tokens;
$pp->{"tokensLength"} = $#tokens;
return $currentToken;
}
method nextToken() {
my @tokens = @{$pp->{"tokens"}};
return $tokens[0];
}
method putToken($token) {
my @tokens = @{ $pp->{"tokens"}};
unshift(@tokens, $token);
$pp->{"tokens"} = \@tokens;
$pp->{"tokensLength"} = $#tokens;
}
}
class FunctionBody {
field $tokens :param;
field $pp = {};
method makeBlockTokens(@tokens) {
$pp->{"blockTokens"} = \@tokens;
$pp->{"blockTokensLength"} = $#tokens;
}
method blockTokensLength() {return $pp->{"blockTokensLength"};}
method getBlockToken() {
my @tokens = @{$pp->{"blockTokens"}};
my $currentToken = shift(@tokens);
$pp->{"blockTokens"} = \@tokens;
$pp->{"blockTokensLength"} = $#tokens;
return $currentToken;
}
method nextBlockToken() {
my @tokens = @{$pp->{"blockTokens"}};
return $tokens[0];
}
method putBlockToken($token) {
my @tokens = @{$pp->{"blockTokens"}};
unshift(@tokens, $token);
$pp->{"blockTokens"} = \@tokens;
$pp->{"blockTokensLength"} = $#tokens;
}
method functionBody() {
use Data::Printer;
$self->makeBlockTokens(@{$tokens});
my $blockTokensLength = $self->blockTokensLength();
my $counter = 0;
while($counter <= $blockTokensLength) {
my $token = $self->getBlockToken(); $counter++;
if($token->{"value"} eq "if") {
my $token=$self->getBlockToken();$counter++;
my @expr;
my $exprToken = {"value" => "_"};
until($exprToken->{"value"} eq ")") {
$exprToken = $self->getBlockToken(); $counter++;
if($exprToken->{"value"} ne ")") {
push(@expr, $exprToken->{"value"});
}
}
my @IfExpr = @expr;
my @ifBody = ();
my $ifBeginToken = $self->getBlockToken();$counter++;
# .....
my $ifBraceCounter = 1;
until($ifBraceCounter <= 0) {
my $tok = $self->getBlockToken();$counter++;
if( $tok->{"value"} eq "{") {
$ifBraceCounter++;
} elsif( $tok->{"value"} eq "}") {
$ifBraceCounter--;
} elsif($ifBraceCounter > 0) {
push(@ifBody, $tok);
} else {$ifBraceCounter--;} #$ifBraceCounter--;
}
use Data::Printer;
p(@ifBody);
}
if($token->{"value"} eq "while") {
my $token = $self->getBlockToken();$counter++;
my @expr;
my $exprToken = {"value" => "_"};
until($exprToken->{"value"} eq ")") {
$exprToken = $self->getBlockToken();$counter++;
if($exprToken->{"value"} ne ")") {
push(@expr, $exprToken->{"value"});
}
}
my @WhileExpr = @expr;
my @whileBody = ();
my $whileBeginToken = $self->getBlockToken();$counter++;
my $whileBraceCounter = 0;
if($whileBeginToken->{"value"} eq "{") { ## .......
$whileBraceCounter++;
my $tok = $self->getBlockToken();$counter++;
if($tok->{"value"} eq "{") {
$whileBraceCounter++;
} elsif($tok->{"value"} eq "}") {
$whileBraceCounter--;
} elsif($whileBraceCounter > 0) {
push(@whileBody, $tok);
} else {} # end this loop
}
}
#my @statement = ();
}
return {"body" => "Functoin Body"};
}
}
class Main {
use Data::Printer;
field $parseTree = {};
method main($program) {
my $parseHelpers = ParseHelpers->new();
my $lexer = Lexer->new();
my @tokens = $lexer->lexer($program);
$parseHelpers->makeTokens(@tokens);
my $tokensLength = $parseHelpers->tokensLength();
my $counter = 0;
my $function;
while($counter <= $tokensLength) {
my $token = $parseHelpers->getToken(); $counter++;
if($token->{"value"} eq 'func') {
my $token = $parseHelpers->getToken(); $counter++;
my $functionName = $token->{"value"};
$parseHelpers->getToken();$counter++;
my @args;
my $argToken = {"value" => "_"};
until( $argToken->{"value"} eq ")") {
$argToken = $parseHelpers->getToken();
$counter++;
if($argToken->{"value"} ne ")"
&& $argToken->{"value"} ne ",") {
push(@args, $argToken->{"value"});
}
}
my @functionArgs = @args;
my @functionBody = ();
my $bodyBeginToken = $parseHelpers->getToken();$counter++;
if($bodyBeginToken->{"value"} eq "{") {
my $untilCounter = 0;
until($untilCounter == 1) {
my $bodyToken = $parseHelpers->getToken();$counter++;
my $bodyNextToken = $parseHelpers->nextToken();
if($bodyToken->{"value"} eq "}"
|| ($bodyNextToken->{"value"} eq "func")
|| $counter == $tokensLength + 1) {
$untilCounter = 1;
}
else {
push(@functionBody, $bodyToken);
}
}
}
my $functionBodyObject = FunctionBody->new(tokens =>\@functionBody);
$function = {
"functionName" => $functionName,
"functionArgs" => \@functionArgs,
"functionBody" => $functionBodyObject->functionBody()
};
$parseTree->{$functionName} = $function;
$function = {};
}
# exit
}
p($parseTree);
}
}
my $program ='
func anotherPrint(arg){
if(x > 23){
print(arg, "\n");
}
}
func main(){
var sum = 12 + 14;
while(sum < 23){
print("Sum is ", sum);
}
}
';
Main->new()->main($program);
I tried to write error free code for more than two days. I'm expecting someone on SO will help me write a error free working code.
EDIT:
line 333 says $tok = {}, but 334 print statemnt prints the value.
I would like the following code to round-trip properly on Perl ≥5.36.
❯ perl -MCpanel::JSON::XS=decode_json,encode_json -Mbuiltin=false,true -MDevel::Peek=Dump -E'
say Cpanel::JSON::XS->VERSION;
Dump { 23 => true };
Dump decode_json encode_json { 23 => true };
'
4.39
SV = IV
SV = PVHV
Elt "23"
SV = PVNV
FLAGS = (IOK,NOK,POK,IsCOW,pIOK,pNOK,pPOK)
PV = "1" [BOOL PL_Yes]
SV = IV
SV = PVHV
Elt "23"
SV = IV
SV = PVMG
FLAGS = (OBJECT,IOK,READONLY,pIOK)
IV = 1
STASH = "JSON::PP::Boolean"
TL;DR
I built a dynamic, context-aware workspace loader for i3wm that allows me to switch between workspace groups (such as "Client" and "Personal") without losing state. It autoloads layouts and launches apps on demand. It's built with Perl, AnyEvent::I3, and YAML config. It utilizes IPC events to detect state changes and trigger actions.
Workspaces on Demand
I have been using i3 as a Window Manager for years now. And while I love working with i3, there is one thing I found annoying. And that is that it lacks context awareness. What do I mean by this? I use it in several contexts: "Client", "Company", and "Personal". Meaning, I'm in either client mode, company mode or personal mode. And 'client mode' can mean working with one or multiple clients at once. I pitched my initial idea on i3's discussion pages and someone else asked about something similar as KDE's "Activities".
Not enough workspaces == mental overload
With i3 you can create a ton of workspaces, but I found myself often using workspaces 1-3, and 8-0. These are only 6 workspaces, that are directly accessible. In most contexts I want to have three workspaces: "debug", "cli", and "misc". If we have three contexts we use 1-3, 4-6, and 7-9 plus workspace 0 as my safe haven. And I use 0 for things I want to super-quick access to regardless of context.
But what if we add a new client, or we want to have different workspaces for "personal" contexts? I didn't want to remember in which context I was, I just wanted to remember, 1-3 are where I code, 0 is where my safe haven is and everything else is just hidden from sight.
Proof of concept: zsh and jq
I needed something that could deal with these contexts while preserving state of other contexts already in use. I started out with a simple zsh script but I quickly ran into some issues where the shell wasn't really the right tool for the job. I have a love/hate relationship with jq
. I love it for quick things, but I hate it for more complex things. I always forget the syntax and with Perl (or any other programming language) I can just work with the data structure.
I also looked into IPC events from i3 — you use them with i3-msg already. Now I wanted to know how I could hook into events from i3 to determine state changes. And I could.
I3 also ships a Perl module AnyEvent::I3. Big hat tip to the i3-developers.
AnyEvent::I3X::Workspace::OnDemand
First I started out with a simple subscribe
method that checked for the init
event to ensure I was able to see the creation of a workspace. It's important to note that while you define a workspace in i3's config, it doesn't actually create them. It is created run time when you request it.
On init
things are rather simple, we append the layout using append_layout
and we start up all the applications we want to use. To support this I created a couple of things:
- A constructor param that has a layout path defined. So all your layouts are stored there.
- A constructor param that has workspaces defined.
- A constructor param that defines which applications are started.
- And a constructor param that defines which groups are used.
How does this work? You configure your workspaces, groups and applications like so:
use AnyEvent::I3X::Workspace::OnDemand;
my $i3 = AnyEvent::I3X::Workspace::OnDemand->new(
debug => 0,
layout_path => "$ENV{HOME}/.config/i3",
workspaces => {
foo => {
layout => 'foo.json',
},
bar => {
layout => 'bar.json',
groups => {
foo => undef,
# Override the layout for group bar
bar => { layout => 'foo.json' },
}
},
baz => {
layout => 'baz.json',
groups => {
all => undef,
}
}
},
groups => [
qw(foo bar baz)
],
swallows => [
{
cmd => 'kitty',
match => {
class => '^kitty$',
}
},
{
# Start firefox on group bar
cmd => 'firefox',
on => {
group => 'bar',
}
match => {
window_role => '^browser$',
}
},
{
cmd => 'google-chrome',
on => {
group => 'foo',
}
match => {
window_role => '^browser$',
}
}
],
);
Bulletproofing edge cases
Now, because of timing issues you can end up with a workspace that has a layout defined but nothing has started yet. So we also look for the focus
event and here we also try to start the applications defined on the workspace.
When the user switches to a workspace, we walk the i3 tree to find any nodes that match our layout, and we spawn the apps that belong there — only if they aren’t already running.
Sending events ourselves to ourselves
So now that we have this logic defined we can go into changing groups. Group changes are sent via ticks
:
i3-msg -t send_tick group:personal
This means our workspace logic needs to listen to tick events too. This meant that I needed to support multiple event types.
Supported event types
The module supports the following event types: workspace, barconfig_update (caveat emptor), tick, shutdown, output, mode, window, and binding. The most important ones for us are: workspace, tick, and shutdown. These have simple helper functions on_workspace
, on_tick
and on_shutdown
:
$self->on_workspace($name, $type, $sub)
Subscribe to a workspace event for workspace $name of $type with $sub.
$type can be any of the following events from i3 plus any or *
$i3->on_workspace(
'www', 'init',
sub {
my $self = shift;
my $i3 = shift;
my $event = shift;
$self->append_layout($event->{current}{name}, '/path/to/layout.json');
}
);
$self->on_tick($payload, $sub)
Subscribe to a tick event with $payload and perform the action. Your sub needs to support the following prototype:
sub foo($self, $i3, $event) {
print "Yay processed foo tick";
}
$self->on_tick('foo', \&foo)
On shutdown isn't really documented on CPAN just yet, but its rather simple:
$i3->on_shutdown(exit => sub { exit 0 });
$i3->on_shutdown(restart => sub { exit 0 });
Configuring i3
So now the only thing you need to do in your i3 config is configure them. I personally use the binding modes from i3 to organize similar commands into one grouping:
# Dynamic workspaces
bindsym $mod+w mode "Activities"
mode "Activities" {
bindsym 0 exec i3-msg -t send_tick group:foo; mode default
bindsym 9 exec i3-msg -t send_tick group:bar; mode default
bindsym 8 exec i3-msg -t send_tick group:baz; mode default
bindsym Return mode "default"
bindsym Escape mode "default"
}
Now I can switch between almost unlimited contexts. I only use three, but you can go wild.
Layout saving and restoring
And for those who want to know how to create a layout:
i3-save-tree --workspace foo > ~/.config/i3/foo.json
You'll need to edit the JSON, eg, trim the swallow
block, but I'll leave that to you and the documentation found on the i3 site.
Version 0.005
In the newest version of the module you'll be able to find the workspace on demand daemon called i3-wod
. The module also ships a user guide for easier adaptation and config examples for the YAML config file used by i3-wod
.
exec_always --no-startup-id "i3-wod &"
You can install the module by running:
cpanm AnyEvent::I3X::Workspace::OnDemand
A few of my recent projects—like Cooking Vinyl Compilations and ReadABooker—aim to earn a little money via affiliate links. That only works if people actually find the pages, share them, and get decent previews in social apps. In other words: the boring, fragile glue of SEO and social meta tags matters.
As I lined up a couple more sites in the same vein, I noticed I was writing very similar code again and again: take an object with title
, url
, image
, description
, and spray out the right <meta>
tags for Google, Twitter, Facebook, iMessage, Slack, and so on. It’s fiddly, easy to get 80% right, and annoying to maintain across projects. So I pulled it into a small Moo role—MooX::Role::SEOTags
—that any page-ish class can consume and just emit the right tags.
What are these tags and why should you care?
When someone shares your page, platforms read a handful of standardised tags to decide what to show in the preview:
- Open Graph (
og:*
) — The de-facto standard for title, description, URL, image, and type. Used by Facebook, WhatsApp, Slack, iMessage and others. - Twitter Cards (
twitter:*
) — Similar idea for Twitter/X; the common pattern istwitter:card=summary_large_image
plus title/description/image. - Classic SEO tags —
<title>
,<meta name="description">
, and a canonical URL tell search engines what the page is about and which URL is the “official” one.
MooX::Role::SEOTags gives you one method that renders all of that, consistently, from your object’s attributes.
For more information about OpenGraph, see ogp.me.
What it does
MooX::Role::SEOTags
adds a handful of attributes and helper methods so any Moo (or Moose) class can declare the bits of information that power social previews and search snippets, then render them as HTML.
- Open Graph tags (
og:title
,og:type
,og:url
,og:image
, etc.) - Twitter Card tags (
twitter:card
,twitter:title
,twitter:description
,twitter:image
) - Standard SEO:
<title>
, metadescription
, canonical<link rel="canonical">
- A single method to render the whole block with one call
- But also individual methods to give you more control over tag placement
That’s the whole job: define attributes → get valid tags out.
Quick start
Install the role using your favourite CPAN module installation tool.
cpanm MooX::Role::SEOTags
Then, in your code, you will need to add some attributes or methods that define the pieces of information the role needs. The role requires four pieces of information – og_title
, og_description
, og_url
and og_type
– and og_image
is optional (but highly recommended).
So a simple class might look like this:
package MyPage; use Moo; with 'MooX::Role::SEOTags'; # minimal OG fields has og_title => (is => 'ro', required => 1); has og_type => (is => 'ro', required => 1); # e.g. 'article' has og_url => (is => 'ro', required => 1); has og_description => (is => 'ro'); # optional niceties has og_image => (is => 'ro'); # absolute URL has twitter_card => (is => 'ro', default => sub { 'summary_large_image' }); 1;
And then you create the object:
my $page = MyPage->new( og_title => 'How to Title a Title', og_type => 'article', og_url => 'https://example.com/post/title', og_image => 'https://example.com/img/hero.jpg', og_description => 'A short, human description of the page.', );
Then you can call the various *_tag
and *_tags
methods to get the correct HTML for the various tags.
The easiest option is to just produce all of the tags in one go:
say $page->tags;
But, for more control, you can call individual methods:
say $page->title_tag; say $page->canonical_tag; say $page->og_tags; # etc...
Depending on which combination of method calls you use, the output will look something like this:
<title>How to Title a Title</title> <meta name="description" content="A short, human description of the page."> <link rel="canonical" href="https://example.com/post/title"> <meta property="og:title" content="How to Title a Title"> <meta property="og:type" content="article"> <meta property="og:url" content="https://example.com/post/title"> <meta property="og:image" content="https://example.com/img/hero.jpg"> <meta name="twitter:card" content="summary_large_image"> <meta name="twitter:title" content="How to Title a Title"> <meta name="twitter:description" content="A short, human description of the page."> <meta name="twitter:image" content="https://example.com/img/hero.jpg">
In many cases, you’ll be pulling the data from a database and displaying the output using a templating system like the Template Toolkit.
my $tt = Template->new; my $object = $resultset->find({ slug => $some_slug }); $tt->process('page.tt', { object => $object }, "$some_slug/index.html");
In this case, you’d just add a single call to the <head> of your page template.
<head> <!-- lots of other HTML --> [% object.tags %] </head>
A note if you used my earlier Open Graph role
If you spotted MooX::Role::OpenGraph
arrive on MetaCPAN recently: SEOTags
is the “grown-up” superset. It does Open Graph and Twitter and standard tags, so you only need one role. The old module is scheduled for deletion from MetaCPAN.
SEO tags and JSON-LD
These tags are only one item in the SEO toolkit that you’d use to increase the visibility of your website. Another useful tool is JSON-LD – which allows you to add a machine-readable description of the information that your page contains. Google loves JSON-LD. And it just happens that I have another Moo role called MooX::Role::JSON_LD which makes it easy to add that to your page too. I wrote a blog post about using that earlier this year.
In conclusion
If you’ve got even one page that deserves to look smarter in search and social previews, now’s the moment. Pick a page, add a title, description, canonical URL and a decent image, and let MooX::Role::SEOTags
spit out the right tags every time (and, if you fancy richer results, pair it with MooX::Role::JSON_LD
). Share the link in Slack/WhatsApp/Twitter to preview it, fix anything that looks off, and ship. It’s a 20-minute tidy-up that can lift click-throughs for years—so go on, give one of your pages a quick SEO spruce-up today.
The post Easy SEO for lazy programmers first appeared on Perl Hacks.
A few of my recent projects—like Cooking Vinyl Compilations and ReadABooker—aim to earn a little money via affiliate links. That only works if people actually find the pages, share them, and get decent previews in social apps. In other words: the boring, fragile glue of SEO and social meta tags matters.
As I lined up a couple more sites in the same vein, I noticed I was writing very similar code again and again: take an object with title
, url
, image
, description
, and spray out the right <meta>
tags for Google, Twitter, Facebook, iMessage, Slack, and so on. It’s fiddly, easy to get 80% right, and annoying to maintain across projects. So I pulled it into a small Moo role—MooX::Role::SEOTags
—that any page-ish class can consume and just emit the right tags.
What are these tags and why should you care?
When someone shares your page, platforms read a handful of standardised tags to decide what to show in the preview:
-
Open Graph (
og:*
) — The de-facto standard for title, description, URL, image, and type. Used by Facebook, WhatsApp, Slack, iMessage and others. -
Twitter Cards (
twitter:*
) — Similar idea for Twitter/X; the common pattern istwitter:card=summary_large_image
plus title/description/image. -
Classic SEO tags —
<title>
,<meta name="description">
, and a canonical URL tell search engines what the page is about and which URL is the “official” one.
MooX::Role::SEOTags gives you one method that renders all of that, consistently, from your object’s attributes.
For more information about OpenGraph, see ogp.me.
What it does
MooX::Role::SEOTags
adds a handful of attributes and helper methods so any Moo (or Moose) class can declare the bits of information that power social previews and search snippets, then render them as HTML.
- Open Graph tags (
og:title
,og:type
,og:url
,og:image
, etc.) - Twitter Card tags (
twitter:card
,twitter:title
,twitter:description
,twitter:image
) - Standard SEO:
<title>
, metadescription
, canonical<link rel="canonical">
- A single method to render the whole block with one call
- But also individual methods to give you more control over tag placement
That’s the whole job: define attributes → get valid tags out.
Quick start
Install the role using your favourite CPAN module installation tool.
cpanm MooX::Role::SEOTags
Then, in your code, you will need to add some attributes or methods that define the pieces of information the role needs. The role requires four pieces of information – og_title
, og_description
, og_url
and og_type
– and og_image
is optional (but highly recommended).
So a simple class might look like this:
package MyPage;
use Moo;
with 'MooX::Role::SEOTags';
# minimal OG fields
has og_title => (is => 'ro', required => 1);
has og_type => (is => 'ro', required => 1); # e.g. 'article'
has og_url => (is => 'ro', required => 1);
has og_description => (is => 'ro');
# optional niceties
has og_image => (is => 'ro'); # absolute URL
has twitter_card => (is => 'ro', default => sub { 'summary_large_image' });
1;
And then you create the object:
my $page = MyPage->new(
og_title => 'How to Title a Title',
og_type => 'article',
og_url => 'https://example.com/post/title',
og_image => 'https://example.com/img/hero.jpg',
og_description => 'A short, human description of the page.',
);
Then you can call the various *_tag
and *_tags
methods to get the correct HTML for the various tags.
The easiest option is to just produce all of the tags in one go:
say $page->tags;
But, for more control, you can call individual methods:
say $page->title_tag;
say $page->canonical_tag;
say $page->og_tags;
# etc...
Depending on which combination of method calls you use, the output will look something like this:
<title>How to Title a Title</title>
<meta name="description" content="A short, human description of the page.">
<link rel="canonical" href="https://example.com/post/title">
<meta property="og:title" content="How to Title a Title">
<meta property="og:type" content="article">
<meta property="og:url" content="https://example.com/post/title">
<meta property="og:image" content="https://example.com/img/hero.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="How to Title a Title">
<meta name="twitter:description" content="A short, human description of the page.">
<meta name="twitter:image" content="https://example.com/img/hero.jpg">
In many cases, you’ll be pulling the data from a database and displaying the output using a templating system like the Template Toolkit.
my $tt = Template->new;
my $object = $resultset->find({ slug => $some_slug });
$tt->process('page.tt', { object => $object }, "$some_slug/index.html");
In this case, you’d just add a single call to the
of your page template.<head>
<!-- lots of other HTML -->
[% object.tags %]
</head>
A note if you used my earlier Open Graph role
If you spotted MooX::Role::OpenGraph
arrive on MetaCPAN recently: SEOTags
is the “grown-up” superset. It does Open Graph and Twitter and standard tags, so you only need one role. The old module is scheduled for deletion from MetaCPAN.
SEO tags and JSON-LD
These tags are only one item in the SEO toolkit that you’d use to increase the visibility of your website. Another useful tool is JSON-LD – which allows you to add a machine-readable description of the information that your page contains. Google loves JSON-LD. And it just happens that I have another Moo role called MooX::Role::JSON_LD which makes it easy to add that to your page too. I wrote a blog post about using that earlier this year.
In conclusion
If you’ve got even one page that deserves to look smarter in search and social previews, now’s the moment. Pick a page, add a title, description, canonical URL and a decent image, and let MooX::Role::SEOTags
spit out the right tags every time (and, if you fancy richer results, pair it with MooX::Role::JSON_LD
). Share the link in Slack/WhatsApp/Twitter to preview it, fix anything that looks off, and ship. It’s a 20-minute tidy-up that can lift click-throughs for years—so go on, give one of your pages a quick SEO spruce-up today.
The post Easy SEO for lazy programmers first appeared on Perl Hacks.
Originally published at Perl Weekly 739
Hi there!
I used Perl Dancer for many projects, I wrote about it a lot, I even wrote a book about it, so I am quite glad that a new major version was released. Thanks to all the contributors, including Jason A. Crome, Mikko Koivunalho, Yanick Champoux, Karen Etheridge, Sawyer X!
Enjoy your week!
Happy and peacefull New Year to all our Jewish readers, Shana Tova!
--
Your editor: Gabor Szabo.
Announcements
Geizhals Preisvergleich Donates USD 10,000 to The Perl and Raku Foundation
Toronto Perl Mongers Lightning Talks Needs You
The annual Toronto Perl Mongers lightning talks will be held (virtually) this Thursday. (ps. It is also listed among all the other events at the bottomg of the newsletter.)
Announcing Dancer2 2.0.0
New major release!
Articles
Released an Inertia.js Adapter for Mojolicious
Discussion
Perl in a Monorepo
A monorepo is a software-development strategy in which the code for a number of projects is stored in the same repository. Dean is interested in hearing about tools and approaches that have been used with Perl.
confusing failed short-circuit
Have I already mentioned one my many pet peeves, that I recommend never to use $a and $b outside of sort. Not even in small examples.
How to make compilation work in Windows?
The Cache Crash
The Weekly Challenge
The Weekly Challenge by Mohammad Sajid Anwar will help you step out of your comfort-zone. You can even win prize money of $50 by participating in the weekly challenge. We pick one champion at the end of the month from among all of the contributors during the month, thanks to the sponsor Lance Wicks.
The Weekly Challenge - 340
Welcome to a new week with a couple of fun tasks "Duplicate Removals" and "Ascending Numbers". If you are new to the weekly challenge then why not join us and have fun every week. For more information, please read the FAQ.
RECAP - The Weekly Challenge - 339
Enjoy a quick recap of last week's contributions by Team PWC dealing with the "Max Diff" and "Peak Point" tasks in Perl and Raku. You will find plenty of solutions to keep you busy.
TWC339
The approaches for both tasks are correct under the assumptions and seem efficient for modest array sizes. The tasks are simple but the solutions are concise and cover many relevant kinds of inputs in the examples.
Diff Peak
The solutions are clear, correct and concise, making good use of Raku’s features. They’re well illustrated with examples, easy to follow and demonstrate a solid understanding of the problem.
Sorting for the win
The solution is much more efficient than brute force and strikes a good balance between simplicity and performance. It uses sound reasoning (extremes drive large products), handles special cases (negatives, zeros) and significantly improves scalability.
Perl Weekly Challenge 339
This is an excellent blog post featuring top-tier solutions. It demonstrates a deep understanding of the problems and chooses the most efficient algorithms possible. The code is not just correct but is also elegant and a pleasure to read. The solutions for both tasks are production-ready and would scale efficiently to large inputs.
Max Complication for Min Brute Force
These solutions are clear, correct and efficient. Max Diff avoids brute force by systematically covering all sign combinations, reducing the problem to a handful of candidate checks after sorting—showing solid algorithmic reasoning and thorough testing. Peak Point is expressed elegantly in a single line, making excellent use of Perl’s expressive features while remaining efficient. Together, the two tasks balance rigor and readability, demonstrating both deep problem understanding and concise, idiomatic coding style.
Points? What’s the diff?
This is an excellent, well-written technical blog post. It successfully transforms a seemingly mundane task—calculating a leaderboard diff—into an engaging narrative that showcases the power and elegance of modern Perl. The post is technically sound, pedagogically effective and demonstrates a strong software engineering mindset. It's a perfect example of how to write about code: it explains the why, not just the how.
Pair products, peak points
Solutions are technically correct, robust and well-engineered. They exemplify a pragmatic approach to problem-solving: prioritizing clarity and guaranteed correctness over premature optimization. The code is modern, idiomatic Perl and is a pleasure to read.
The Weekly Challenge #339
Solutions are technically correct and functionally complete, successfully solving both challenges. The code is structured, well-documented and demonstrates a solid understanding of the problem requirements. However, the approach to Task 1 raises significant concerns regarding efficiency and scalability, which heavily impacts the overall assessment.
The Difference has Peaked
Solutions are solid—clear, correct, straightforward and great for demonstrating the basic logic. For tasks of modest size, they’ll work fine. For larger inputs, they’d be slower than more optimized ones. His decision to trade off some performance for clarity and ease of implementation is reasonable.
Maximum climb
The code is straightforward and easy to follow, making the logic transparent even for less experienced readers. Solutions in both Python and Perl show versatility and consistency in approach. The post acknowledges the brute force nature and performance limitations, which shows awareness of algorithmic complexity.
Week 339 - Max Diff & Peak Point
Solutions are well-structured, demonstrating a solid understanding of the tasks and Perl's capabilities. They are both elegant and efficient, providing a clear and direct answer to the problem.
Weekly collections
NICEPERL's lists
Great CPAN modules released last week;
MetaCPAN weekly report.
Events
Toronto.pm - online - Lightning Talks 2025
September 25, 2025
Annual Russian Perl Conference 2025
September 27, 2025
Boston.pm - online - (2d Tuesday)
October 14, 2025
Toronto.pm - online - How SUSE is using Perl
December 6, 2025
You joined the Perl Weekly to get weekly e-mails about the Perl programming language and related topics.
Want to see more? See the archives of all the issues.
Not yet subscribed to the newsletter? Join us free of charge!
(C) Copyright Gabor Szabo
The articles are copyright the respective authors.
Weekly Challenge 339
Each week Mohammad S. Anwar sends out The Weekly Challenge, a chance for all of us to come up with solutions to two weekly tasks. My solutions are written in Python first, and then converted to Perl. It's a great way for us all to practice some coding.
Task 1: Max Diff
Task
You are given an array of integers having four or more elements.
Write a script to find two pairs of numbers from this list (four numbers total) so that the difference between their products is as large as possible.
In the end return the max difference.
With Two pairs (a, b)
and (c, d)
, the product difference is (a * b) - (c * d)
.
My solution
This task has really challenged me. I've come up with a solution that works, but am not happy with it. Mohammad praised me last week with not "just brute-forcing an answer". This week I have failed :-/
The objective is to find the pairs that represent the lowest and highest product. If I take the list -4 -10 2 0
, the lowest value is -8
(-4 × 2) and the highest value is 40
(-4 × -10). However this uses the -4 value twice, and thus is not going to work. I was thinking of ways to solve this, but haven't thought of any that are good. I'm going to be interested in reading how other Team PWC members solved this.
The approach I took is to find all combinations of 4 digits from the input list. In Python, this is done with the combinations function from itertools. The Algorithm::Combinatorics module provides the same functionality in Perl.
For each combination I calculate the maximum absolute difference between the three possible combinations of pairs (1 2, 3 4; 1 3, 2 4; and 1 4, 2 3). If this is higher than any previous maximum, I update the solution
variable.
def max_diff(ints: list) -> int:
solution = 0
# Compute all combinations of 4 integers
for c in combinations(ints, 4):
# Calculate the maximum difference for this combination
diff = max(
abs((c[0] * c[1]) - (c[2] * c[3])),
abs((c[0] * c[2]) - (c[1] * c[3])),
abs((c[0] * c[3]) - (c[1] * c[2])),
)
if diff > solution:
solution = diff
return solution
Maybe this is the best solution. Even with a list of 100 items, there are less than 4 million combinations, which can be computed pretty quickly.
Examples
$ ./ch-1.py 5 9 3 4 6
42
$ ./ch-1.py 1 -2 3 -4
10
$ ./ch-1.py -3 -1 -2 -4
10
$ ./ch-1.py 10 2 0 5 1
50
$ ./ch-1.py 7 8 9 10 10
44
Task 2: Peak Point
Task
You are given an array of altitude gain.
Write a script to find the peak point gained.
My solution
This is relatively straight forward so doesn't need much explanation. I have two variables current_altitude
records the current altitude while the peak
records the highest altitude. Both values start at 0
.
I then loop over the input list with a variable gain
. For each iteration, I increment the current_altitude
value with gain
. If this is higher than the current peak
variable, I update peak
with the new altitude.
Finally, I return the peak
value.
def peak_point(gains: list) -> int:
current_altitude = 0
peak = 0
for gain in gains:
current_altitude += gain
if current_altitude > peak:
peak = current_altitude
return peak
Examples
$ ./ch-2.py -5 1 5 -9 2
1
$ ./ch-2.py 10 10 10 25
55
$ ./ch-2.py 3 -4 2 5 -6 1
6
$ ./ch-2.py -1 -2 -3 -4
0
$ ./ch-2.py -10 15 5
10
-
Dancer2 - Lightweight yet powerful web application framework
- Version: 2.0.0 on 2025-09-15, with 138 votes
- Previous CPAN version: 1.1.2 was 9 months, 20 days before
- Author: CROMEDOME
-
EV - perl interface to libev, a high performance full-featured event loop
- Version: 4.36 on 2025-09-19, with 49 votes
- Previous CPAN version: 4.35 was before
- Author: MLEHMANN
-
Firefox::Marionette - Automate the Firefox browser with the Marionette protocol
- Version: 1.68 on 2025-09-13, with 18 votes
- Previous CPAN version: 1.67 was 2 months, 29 days before
- Author: DDICK
-
Future::AsyncAwait - deferred subroutine syntax for futures
- Version: 0.71 on 2025-09-16, with 49 votes
- Previous CPAN version: 0.70 was 8 months, 26 days before
- Author: PEVANS
-
Minilla - CPAN module authoring tool
- Version: v3.1.28 on 2025-09-15, with 98 votes
- Previous CPAN version: v3.1.27 was 1 month, 3 days before
- Author: SKAJI
-
SPVM - The SPVM Language
- Version: 0.990090 on 2025-09-17, with 36 votes
- Previous CPAN version: 0.990089 was 1 day before
- Author: KIMOTO
-
URI - Uniform Resource Identifiers (absolute and relative)
- Version: 5.34 on 2025-09-17, with 119 votes
- Previous CPAN version: 5.33 was before
- Author: OALDERS
This is the weekly favourites list of CPAN distributions. Votes count: 53
Week's winner: XS::Log (+2)
Build date: 2025/09/20 18:00:27 GMT
Clicked for first time:
- Data::Censor - censor sensitive stuff in a data structure
- Duadua - Detect User-Agent, do up again!
- FU - A Lean and Efficient Zero-Dependency Web Framework.
- Iterator::Flex - Iterators with flexible behaviors
- Minima - Efficient web framework built with modern core classes
- Mojolicious::Plugin::Inertia - Inertia.js adapter for Mojolicious
- PDK::Connector - PDK-Connector
- Perl5::TestEachCommit - Test each commit in a pull request to Perl core
- Sys::Async::Virt - LibVirt protocol implementation for clients
- XString - Isolated String helpers from B
Increasing its reputation:
- App::Codit (+1=3)
- App::cpanminus (+1=284)
- App::find2perl (+1=2)
- App::perlimports (+1=20)
- boolean (+1=22)
- Carmel (+1=36)
- CGI::Info (+1=2)
- CHI (+1=63)
- Class::Tiny (+1=39)
- CryptX (+1=53)
- Data::Dumper::Simple (+1=5)
- DBIx::RunSQL (+1=7)
- Digest::HMAC (+1=12)
- Disk::SMART (+1=4)
- EV (+1=49)
- Feature::Compat::Class (+1=8)
- Feature::Compat::Defer (+1=4)
- File::Slurper (+1=44)
- Future (+1=62)
- Future::HTTP (+1=3)
- Homer (+1=2)
- Image::ExifTool (+1=44)
- IO::Prompter (+1=28)
- Log::Any (+1=67)
- meta (+1=14)
- Minion (+1=109)
- Module::Load (+1=32)
- Number::Phone (+1=20)
- OpenAPI::Client::OpenAI (+1=2)
- PLS (+1=18)
- Prima (+1=46)
- Prima::Cairo (+1=4)
- results (+1=2)
- String::Compare::ConstantTime (+1=4)
- String::Random (+1=23)
- Template::Toolkit (+1=149)
- Test::Deep (+1=55)
- Test::Simple (+1=197)
- Util::H2O (+1=12)
- XS::Log (+2=2)
- YAML::PP (+1=20)
- YAML::Syck (+1=18)
What’s SibSoft anyway? SibSoft is a company that sells file-sharing and video hosting scripts (think XFileSharing Pro and friends). A lot…
Today The Perl and Raku Foundation is thrilled to announce a donation of USD 10,000 from Geizhals Preisvergleich. This gift helps to secure the future of The Perl 5 Core Maintenance Fund.
Perl has been an integral part of our product price comparison platform from the start of the company 25 years ago. Supporting the Perl 5 Core Maintenance Fund means supporting both present and future of a substantial pillar of Modern Open Source Computing, for us and other current or prospective users.
– Michael Kröll of Geizhals Preisvergleich
“Geizhals is not only providing core funding for the Perl ecosystem, but also supporting developers, actively contributing to European conferences, and employing Perl coders. Their interest in the strategic maintenance and development of Perl and CPAN is of great value to us all, and their investment is very much appreciated.”
– Stuart J Mackintosh, President of The Perl and Raku Foundation
But who exactly is Geizhals, and why does their support matter so much to the Perl community?
Geizhals Preisvergleich began in July of 1997 as a hobby project—and yes, “Geizhals” literally translates to “skinflint” in English (they even operate skinflint.co.uk for UK users!). From those humble beginnings, they’ve leveraged the power of Perl to scale up to serving 4.3 million monthly users. With Perl being a key part of their infrastructure, they have generously decided to support the Perl 5 Core Maintenance Fund.
While many of us know about the Core Maintenance Fund, the specific problems it addresses often remain invisible to users. I reached out to the maintainers whose work is supported by this fund. This is what core maintainer Tony Cook had to say:
My work tends to be little things, I review other people’s work which I think improves quality and velocity, and fix more minor issues, some examples would be:
a fix to signal handling where perl could crash where an external library created threads (#22487)
fix a segmentation fault in smartmatch against a sub if the sub exited via a loop exit op (such as last) (#16608)
fixed a bug where a regexp warning could leak memory.
prevent a confusing undefined warning message when accessing a sub parameter that was placeholder for a hash element indexed by an undef key (#22423)
What Tony has highlighted are the kinds of bug fixes which collectively help to ensure that Perl remains stable, secure and reliable for the many organisations and individuals who depend on it.
With organizations like Geizhals Preisvergleich funding the work which Tony and others put into maintaining the Perl 5 core, we can work together to ensure that the Perl core continues to receive the maintenance which it deserves, for many years to come. Whether you’re a startup using Perl for rapid prototyping or an enterprise running mission-critical systems, your support helps ensure Perl remains reliable for everyone. Please join us on this journey.
For more information on how to become a sponsor, please contact: olaf@perlfoundation.org
Your Favorite Perl Web Framework, Now Even Better
The Dancer Core Team project is proud to announce the release of Dancer2 2.0.0!
This release has been a long time coming, and while open source sometimes takes longer than we’d like, we believe the wait has been worth it. With fresh documentation, architectural improvements, and developer-friendly new features, version 2.0.0 represents a significant evolution of Dancer2 and the Perl web ecosystem.
If you’d like a more extensive overview, I gave a talk at the Perl and Raku Conference 2025, about Dancer2 2.0.0, covering new features and where we’re headed next. You can watch the full presentation here: Dancer2 2.0.ohh myyy on YouTube →
Release Highlights
Every major release is an opportunity to take stock of where a project stands and where it’s going. For Dancer2, 2.0.0 represents:
- A renewed commitment to documentation and developer experience
- A modernization of the framework’s core
- A better foundation for web development with Perl
Here are some of the most important changes included in Dancer2 2.0.0:
-
Brand New Documentation Thanks to a grant from the Perl and Raku Foundation, Dancer2’s documentation has been completely rewritten. Clearer guides and reference materials will make it easier than ever to get started and to master advanced features. Read the docs →
-
Extendable Configuration System Thanks to first-time contributor Mikko Koivunalho, the new configuration system allows for greater flexibility in how Dancer2 applications are configured and extended. Developers can build new configuration modules, and even integrate configuration systems from other applications or frameworks.
Building on this work, long-time core team member Yanick Champoux enhanced the new configuration system even further, enabling additional configuration readers to be bootstrapped by the default one. Learn more in the config guide →
-
A Leaner Core Distribution Why have two templating systems in the core framework when you can have zero?
Dancer2::Template::Simple
has been removed from the core. It is now available as a separate distribution on MetaCPAN for migration projects from Dancer 1.- Our fork of
Template::Tiny
has been retired, with its improvements merged upstream (thanks, Karen Etheridge!), andDancer2::Template::TemplateTiny
is now just an adapter for the official version. See the documentation →
-
Smarter Data Handling Dancer2 now supports configurable data/secrets censoring using
Data::Censor
, helping developers protect sensitive information in logs and debug pages. Learn more about Data::Censor → -
Better Logging and Debugging Hooks are now logged as they are executed, and a brand-new hook —
on_hook_exception
— provides a way to handle unexpected issues more gracefully. See the hooks documentation → -
CLI Improvements The command-line interface also received some attention:
- Allows new Dancer2 applications to be scaffolded from the Dancer2 Tutorial.
- Behind-the-scenes improvements to allow future scaffolding of plugins and extensions.
Thank You to Our Contributors
Dancer2 is what it is because of its community. A heartfelt thank you goes out to everyone who made this release possible, including:
- Mikko Koivunalho, for his first-time contribution of the extendable configuration system.
- Yanick Champoux, for building on Mikko’s work and extending what’s possible with the new configuration system.
- Karen Etheridge, for integrating improvements back into
Template::Tiny
, helping us to streamline the Dancer2 core. - The Perl and Raku Foundation, for supporting the documentation grant that gave us our brand-new docs.
- Sawyer X, for being a great grant manager, and his endless patience, great suggestions, and encouragement throughout the grant process.
- And of course, everyone who tested, reported issues, submitted patches, and offered feedback along the way.
Your contributions and support are what keep the project moving forward. ❤️
What’s Next?
Dancer2 remains an active, community-driven project, and version 2.0.0 shows our continued dedication to advancing the framework and supporting our community.
We invite you to try out Dancer2 2.0.0, explore the new documentation, and join the conversation on GitHub or the project’s mailing list.
Keep Dancing!
We’re excited about this release and can’t wait to see what the Perl community builds with it.
Jason (CromeDome) and the Dancer2 Core Team
-
App::Netdisco - An open source web-based network management tool.
- Version: 2.089002 on 2025-09-12, with 753 votes
- Previous CPAN version: 2.089001 was 4 days before
- Author: OLIVER
-
Convert::Binary::C - Binary Data Conversion using C Types
- Version: 0.86 on 2025-09-07, with 14 votes
- Previous CPAN version: 0.85 was 11 months, 5 days before
- Author: MHX
-
Cpanel::JSON::XS - cPanel fork of JSON::XS, fast and correct serializing
- Version: 4.40 on 2025-09-08, with 46 votes
- Previous CPAN version: 4.39 was 8 months, 26 days before
- Author: RURBAN
-
Excel::Writer::XLSX - Create a new file in the Excel 2007+ XLSX format.
- Version: 1.15 on 2025-09-13, with 103 votes
- Previous CPAN version: 1.14 was 10 months, 22 days before
- Author: JMCNAMARA
-
Image::ExifTool - Read and write meta information
- Version: 13.36 on 2025-09-09, with 44 votes
- Previous CPAN version: 13.35 was 3 days before
- Author: EXIFTOOL
-
JSON::Schema::Modern - Validate data against a schema using a JSON Schema
- Version: 0.618 on 2025-09-07, with 12 votes
- Previous CPAN version: 0.617 was 21 days before
- Author: ETHER
-
JSON::XS - JSON serialising/deserialising, done correctly and fast
- Version: 4.04 on 2025-09-08, with 119 votes
- Previous CPAN version: 4.03 was 4 years, 10 months, 11 days before
- Author: MLEHMANN
-
LWP - The World-Wide Web library for Perl
- Version: 6.80 on 2025-09-11, with 207 votes
- Previous CPAN version: 6.79 was 2 months, 14 days before
- Author: OALDERS
-
MCE - Many-Core Engine for Perl providing parallel processing capabilities
- Version: 1.902 on 2025-09-09, with 112 votes
- Previous CPAN version: 1.901 was 8 months, 6 days before
- Author: MARIOROY
-
Net::Ping - check a remote host for reachability
- Version: 2.76 on 2025-09-08, with 15 votes
- Previous CPAN version: 2.75 was 3 years, 2 days before
- Author: RURBAN
-
Number::Phone - base class for Number::Phone::* modules
- Version: 4.0008 on 2025-09-13, with 24 votes
- Previous CPAN version: 4.0007 was 3 months, 7 days before
- Author: DCANTRELL
-
Perl::Tidy - indent and reformat perl scripts
- Version: 20250912 on 2025-09-12, with 143 votes
- Previous CPAN version: 20250711 was 2 months, 1 day before
- Author: SHANCOCK
-
Set::Object - Unordered collections (sets) of Perl Objects
- Version: 1.43 on 2025-09-08, with 12 votes
- Previous CPAN version: 1.42 was 3 years, 7 months, 17 days before
- Author: RURBAN
-
SNMP::Info - OO Interface to Network devices and MIBs through SNMP
- Version: 3.974000 on 2025-09-11, with 39 votes
- Previous CPAN version: 3.973000 was 7 days before
- Author: OLIVER
-
SPVM - The SPVM Language
- Version: 0.990088 on 2025-09-11, with 36 votes
- Previous CPAN version: 0.990087 was before
- Author: KIMOTO

Paul writes:
In August I focused on progressing my work on sub signatures. Between
the main OP_MULTIPARAM
work and the surrounding supporting changes,
we're now much better placed to look at no-snails or signatures named
parameters.
- 17 = OP_MULTIPARAM and related changes, prerequisites and post-changecleanups
- https://github.com/Perl/perl5/pull/23539
- https://github.com/Perl/perl5/pull/23544
- https://github.com/Perl/perl5/pull/23565
- https://github.com/Perl/perl5/pull/23574
- https://github.com/Perl/perl5/pull/23645
Total: 17 hours

Tony writes: ``` 2025/08/04 Monday 0.13 github notifications 2.37 #23483 see if this can work for netbsd, testing, testing on openbsd, freebsd and comment 0.15 #23483 testing based on IRC
1.37 #23519 review, research, testing and approve
4.02
2025/08/05 Tuesday 1.33 #21877 research (do I need to rewrite this?)
1.43 #21877 work on test code
2.76
2025/08/06 Wednesday 0.47 #23503 research 0.52 #23542 review and comment 0.43 #23539 review and approve 0.15 #23537 review and approve 0.32 #23542 review update and comment
2.10 #16865 debugging
3.99
2025/08/07 Thursday 0.28 #14630 testing and comment 0.08 #18786 review discussion and comment 0.38 #16808 research, testing and comment 0.42 #10376 review discussion, research and comment 0.68 #23543 review, comments 0.08 #23542 review updates and approve 0.08 #23422 apply to blead 0.18 #23459 review and approve
1.37 #16865 debugging and comment
3.55
2025/08/11 Monday 1.00 #23544 review and approve 0.38 #23546 review change, review history and comment 0.60 #23553 review, comments 0.47 #23555 review and comment 0.23 #23557 review and approve
1.28 #16865 debugging
3.96
2025/08/12 Tuesday 0.45 #23543 review discussion, research and comment 0.13 #23555 review updates and comment 0.90 #23375 review comments and long comment 0.08 #23555 review updates and approve 0.08 #23563 review and approve 0.25 #16808 comment 0.35 #10385 comment 1.27 #15004 review discussion and patch, work on an
alternative, testing and push for CI
3.51
2025/08/13 Wednesday 0.40 github notifications 0.63 check coverity reported error and push a fix for CI 1.33 #23202 hopefully final review and comment (minor issue) 0.10 #16715 briefly research and comment 0.08 #15004 check CI, make PR 23567 0.10 coverity: check CI and make PR 23568 0.25 #15004 minor fix, testing 0.08 #23568 apply to blead
1.67 #23503 review, research
4.64
2025/08/14 Thursday 0.67 #23543 review updates, research and approve 0.78 #23202 review updates and comment 0.72 #23565 review and approve 0.87 #23561 research and comment
0.62 #23570 review, comment, note failed CI and cause
3.66
2025/08/18 Monday 0.08 #23567 review discussion, apply to blead 0.48 #23561 longish comment 1.22 #23570 review updates, struggle to understand some code, comment 0.27 #23202 review updates and approve 0.72 #23503 review, comment 0.20 #23573 comment
0.57 #23553 review discussion, comment
3.54
2025/08/19 Tuesday 0.62 #23533 review discussion as requested by khw, discussion with khw 0.72 check new coverity scan report
1.55 #16865 debugging
2.89
2025/08/20 Wednesday 0.95 #23570 review updates, research and approve 0.58 #23561 research and comments 0.97 #23574 reviewing...
1.35 #23574 more reviewing
3.85
2025/08/21 Thursday 1.58 #23574 more review and approve 0.60 #13140 review discussion, testing and comment 0.47 #8468 review discussion, research, testing and comment
1.20 #23608 testing and comment
3.85
2025/08/25 Monday 0.42 discuss handle_possible_posix with khw (while setting up to test Dennis Clark’s list reported FreeBSD failure, and testing)
0.87 more FreeBSD testing, respond to Dennis
1.29
2025/08/26 Tuesday 1.03 #23647 review, testing, generated code checks, comments 0.15 #23647 review update and approve 0.62 #23645 review, review CI results, testing and comment 0.43 #23644 review the involved tickets, some testing and
briefly comment
2.23
2025/08/27 Wednesday 0.18 #16865 follow-up 0.88 #23640 review and approve 0.40 #23638 review, suggest and alternative 0.08 #23634 review and approve 0.27 #23632 review, research and comment 0.23 #23627 review and comment 0.08 #23621 review and approve 0.32 #23616 review, research and comments
1.40 #16865 debugging
3.84
2025/08/28 Thursday 0.60 #23654 review, research and approve 0.60 #23645 review, research, testing and approve, comment 1.15 #23641 review, testing, research and comment 0.22 #23613 review and approve 0.23 #23607 review and approve 0.48 #23612 research and comment
1.27 #16865 debugging
4.55
Which I calculate is 56.13 hours.
Approximately 52 tickets were reviewed or worked on, and 3 patches were applied. ```

Dave writes:
I spent last month mainly continuing to work on rewriting and modernising perlxs.pod, Perl's reference manual for XS. The first draft is now about 90% complete. (Last month it was 80%; no doubt next month it will be 95%, then 97.5%, etc.) The bits that have been reworked so far have ended up having essentially none of the original text left, apart from section header titles (which are now in a different order). So it's turning into a complete rewrite from scratch.
It's still a work-in-progress, so nothing's been pushed yet.
During the course of writing about the XS INTERFACE keyword, I discovered a bug and fixed it; I also took the opportunity of fixing another INTERFACE bug which had been reported recently, where the C code generated was giving errors on recent picky C compilers.
Summary:
- 12:26 fix issues with the XS INTERFACE keyword
- 33:58 modernise perlxs.pod
Total:
- 46:24 (HH::MM)
This week Farhad and me finally found some time to improve a part of our build pipeline that was nagging me for years. We can now release our DarkPAN modules via CI/CD into a GitLab generic packages repository and install them from there into our app containers, also via CI/CD pipelines.
But before we start with the details, a little background:
DarkPAN
Perl modules are published to CPAN. But not all Perl code goes to CPAN, and this "dark Perl matter" is called "DarkPAN". You especially don't want to publish the internal code that's running your company, or the set of various helper code that you collect over the years that's not really tied to one app, and also (for whatever reasons) not suitable to be properly release to CPAN. If you still want to use all the best practices and tools established in the last 30+ years, but in private, you can set up an internal CPAN-like repository, for example using tools like Pinto. Then you can use standard tools to install your internal dependencies from your internal CPAN-like repo (which I also call DarkPAN).
So this is what we did in the last ~10 years:
- We have a bunch of applications
- We have a bunch of libraries that are used by those applications
- When we push a library repo, GitLab will run a CI job that
* uses Dist::Zilla to build a proper package for the library
* also uses Dist::Zilla to release the package to our custom Pinto - When an app needs to use such a library (or a new version), we add it to the apps
cpanfile
- And when a new release of the app is built (again via GitLab CI/CD pipeline), we use cpm with a custom
resolver
to install the dependency from our DarkPAN
cpm install -g --resolver metadb --resolver 02packages,https://pinto.internal.example.com/stacks/darkpan
This worked quite well.
Shared Libs
It worked so well that we also used this to release and install what I now call "shared libs" inside monorepos: We have a few monorepos for different projects, where each monorepo contains multiple apps (different API backends, frontends and anything in between). Inside a monorepo we have code that we want to share between all apps, for example the database abstraction (DBIx::Class). Deploying these shared libs via the above method was working, but not very smoothly, especially when there's a lot of development happening: You had to push a new version of the lib, wait for it to be released, and then bump the version in all the cpanfiles
using this library.
So a few weeks ago I changed our handling of these shared libs. Instead of properly installing them via cpm
, I can copy the source code directly into the app container and set PERL5LIB
accordingly. This is possible because all of the code is in the same monorepo and thus available in the CI/CD pipeline. (material for another blog post..)
This hack is not an (easy) option for code that has to be shared between projects. But I wanted to get rid of maintaining a server to host Pinto, especially as we already have GitLab running, which supports a large range of language specific repositories. Unfortunately, Perl/CPAN is not implemented. But they have a "generic" repository, so I tried to use it to solve our problem.
Publishing a Perl package to into a GitLab generic packages repository
The first step is to publish the freshly build Perl distribution into the GitLab repo. This is easy to do via the GitLab API and curl
. The API endpoint is not very nice IMO: api/v4/projects/{project-id}/packages/generic/{name}/{version}/{file}
, and I find it a bit weird that you set up a name
and version
and then add files to it (instead of just uploading a tarball), but whatever.
In our Makefile
we added:
package_registry := "https://gitlab.example.com/api/v4/projects/foo%2Fbar/packages/generic/$(application_name)"
get_version = $(shell ls $(application_name)-*.tar.gz | sed "s/$(application_name)-//; s/\.tar\.gz//")
get_filename = $(shell ls $(application_name)-*.tar.gz)
private_token ?= ${CI_JOB_TOKEN}
.PHONY: build
build:
dzil authordeps --missing | xargs -r cpm install --global
cpm install --global
dzil build
.PHONY: release
release:
$(eval VERSION := $(get_version))
$(eval FILENAME := $(get_filename))
curl -L -H "JOB-TOKEN: $(private_token)" \
--upload-file "$(FILENAME)" \
"$(package_registry)/$(VERSION)/$(FILENAME)"
We set the package_registry
base URL (note that I use the URI-escaped string foo%2Fbar
to address the project bar
in group foo
, because I find using the project ID even uglier that escaping the /
as %2F
). Then we use some shell / sed to "parse" the filename and get the version number (which is set automatically via Dist::Zilla) in build
.
In release
, we construct the final URL, authorize using the CI_JOB_TOKEN
and call curl
to upload the file.
Why are we using a Makefile
instead of defining the steps in .gitlab-ci.yml
?
- I can use the
Makefile
locally, so I can run whatever steps that are triggered by the pipeline without having to push a change to gitlab (no moreforce pipeline
commits!) - We prefer writing
Makefile
to "programming" in YAML - The CI stages are very short:
script: - make build release
Installing from GitLab
Actually installing the Perl distribution from GitLab seemed easy, but GitLab threw a few stumbling blocks in our way.
My plan was to just take the URL of the distribution in the GitLab Generic Registry and use that in cpanfile
via the nice url
addon which allows you to install a CPAN distribution directly from the given URL:
requires 'Internal::Package' => '1.42', url => 'https://gitlab.example.com/foo/bar/packages/generic/Internal-Package-1.42.tar.gz';
But for weird reasons, GitLab does not provide a simple URL like that, esp not for unauthorized users. Instead you have to call the API, provide a token and only then you can download the tarball. And there is no way to add a custom header to pass the token in cpanfile
.
After some more reading of the docs, we found that instead of using a header, we can also stuff a deploy token into basic auth using https://user:password@url
format, and thus can specify the install URL like this:
requires 'Internal::Package' => '1.42', url => 'https://deployer:gldt-234lkndfg@gitlab.example.com/foo/bar/packages/generic/Internal-Package-1.42.tar.gz';
And this works!!
Well, the URL (using the actual API call) in fact looks like this:
requires 'Internal::Package' => '1.42', url =>
'https://deployer:gldt-234lkndfg@gitlab.example.com/api/v4/projects/foo%2Fbar/packages/generic/Internal-Package/1.42/Internal-Package-1.42.tar.gz/';
This is not very nice:
- The token is embedded in the
cpanfile
- We have to define the package name three times
- And we also have to define the version three times, and adapt it correctly every time we release a new version
So we continued to improve this in a maybe crazy but perlish way:
Dynamic cpanfile
One of the nice things of cpanfile
(and Perl in general) is that instead of inventing some stupid DSL to specify your requirements, we just use code:
requires 'Foo::Bar';
is actually calling a function somewhere that does stuff.
So we can run code in cpanfile
:
my @DB = qw(Pg mysql DB2 CSV);
my $yolo_db = 'DB::' . $DB[rand @DB];
requires $yolo_db;
The above code is of course crazy, but we can use this power for good and write a nice little wrapper to make depending on our DarkPAN easier.
Validad::InstallFromGitlab
I wrote a small tool, Validad::InstallFromGitlab
, which is configured with the GitLab base URL, the project name and the token. It exports a function from_gitlab
, which takes the name and the version of the distribution and returns the long line that requires
needs.
And because cpanfile
is just Perl, we can easily use this module there:
use Validad::InstallFromGitlab (gitlab => 'https://gitlab.example.com', project => 'validad%2fcontainer', auth => $ENV{DARKPAN_ACCESS});
requires from_gitlab( 'Validad::Mailer' => '1.20250904.144530');
requires from_gitlab( 'Accounts::Client' => '1.20250904.144543');
I decided to use some rather old but very powerful method to make Validad::InstallFromGitlab
easy to use: A custom import()
function:
package Validad::InstallFromGitlab;
use v5.40;
use Carp qw(croak);
sub import {
my ($class, %args) = @_;
if (!$args{gitlab} || !$args{project}) {
croak("gitlab and/or project missing");
}
my $registry_url = sprintf('%s/api/v4/projects/%s/packages/generic', $args{gitlab}, $args{project});
if (my $auth = $args{auth}) {
$registry_url =~s{://}{'://'.$auth.'@'}e;
}
my $caller=caller();
no strict 'refs';
*{"$caller\::from_gitlab"} = sub {
my ($module, $version) = @_;
my $package = $module;
$package =~s/::/-/g;
my $tarball = $package .'-' . $version . '.tar.gz' ;
my $url = join('/',$registry_url, $package, $version, $tarball);
return ($module, url => $url);
};
}
1;
import()
is called when you use the module in the calling code or cpanfile
:-)
use Validad::InstallFromGitlab (
gitlab => 'https://gitlab.example.com',
project => 'validad%2fcontainer',
auth => $ENV{DARKPAN_ACCESS}
);
The parameters passed to use
are passed on to import
, where I do some light checking and build the long and cumbersome GitLab url.
Then I use caller()
to get the name of the calling namespace and use a typoglob (been some time since I used that..) to install a function named from_gitlab
into the caller.
This function takes two params, the module name and version, and finally constructs the data needed by require, i.e. the module name, version and the whole gitlab URL.
So I can now specify my requirements very easily in the apps cpanfiles
and still use the GitLab generic package registry to distribute my DarkPAN modules!
Installing Validad::InstallFromGitlab
But how do I install Validad::InstallFromGitlab
?
I don't. But all of our apps use a shared base container (which also helps to keep container size down). And in the Containerfile of the base container, I copy Validad::InstallFromGitlab
to a well-known location /opt/perl/darkpan
and load it from there via PERL5LIB
:
RUN mkdir -p /opt/perl/darkpan/Validad/
COPY Validad-InstallFromGitlab.pm /opt/perl/darkpan/Validad/InstallFromGitlab.pm
ONBUILD ARG DARKPAN_ACCESS
ONBUILD RUN PERL5LIB=/opt/perl/darkpan/ \
/opt/perl/bin/cpm install --cpanfile cpanfile \
--show-build-log-on-failure -g
But again that's material for another blog post...
-
App::Netdisco - An open source web-based network management tool.
- Version: 2.088004 on 2025-09-05, with 751 votes
- Previous CPAN version: 2.088003 was 2 days before
- Author: OLIVER
-
App::rdapper - a simple console-based RDAP client.
- Version: 1.19 on 2025-09-01, with 21 votes
- Previous CPAN version: 1.18 was 1 month, 2 days before
- Author: GBROWN
-
File::Temp - return name and handle of a temporary file safely
- Version: 0.2312 on 2025-09-01, with 71 votes
- Previous CPAN version: 0.2311 was 4 years, 10 months, 29 days before
- Author: ETHER
-
Glib::Object::Introspection - Dynamically create Perl language bindings
- Version: 0.052 on 2025-09-03, with 16 votes
- Previous CPAN version: 0.051 was 2 years, 5 days before
- Author: XAOC
-
Image::ExifTool - Read and write meta information
- Version: 13.35 on 2025-09-06, with 43 votes
- Previous CPAN version: 13.30 was 3 months, 11 days before
- Author: EXIFTOOL
-
IO::Prompter - Prompt for input, read it, clean it, return it.
- Version: 0.005004 on 2025-09-05, with 27 votes
- Previous CPAN version: 0.005003 was 7 days before
- Author: DCONWAY
-
meta - meta-programming API
- Version: 0.014 on 2025-09-03, with 13 votes
- Previous CPAN version: 0.013 was 4 months, 9 days before
- Author: PEVANS
-
Net::MQTT::Simple - Minimal MQTT version 3 interface
- Version: 1.33 on 2025-09-02, with 12 votes
- Previous CPAN version: 1.32 was 4 months, 9 days before
- Author: JUERD
-
Net::SIP - Framework SIP (Voice Over IP, RFC3261)
- Version: 0.839 on 2025-09-04, with 16 votes
- Previous CPAN version: 0.838 was 1 year, 1 month, 2 days before
- Author: SULLR
-
Sisimai - Mail Analyzing Interface for bounce mails.
- Version: v5.4.1 on 2025-08-31, with 80 votes
- Previous CPAN version: v5.4.0 was 1 month, 29 days before
- Author: AKXLIX
-
SNMP::Info - OO Interface to Network devices and MIBs through SNMP
- Version: 3.973000 on 2025-09-04, with 39 votes
- Previous CPAN version: 3.972002 was 10 months, 5 days before
- Author: OLIVER
-
Type::Tiny - tiny, yet Moo(se)-compatible type constraint
- Version: 2.008003 on 2025-09-02, with 144 votes
- Previous CPAN version: 2.008002 was 4 months, 1 day before
- Author: TOBYINK