Author: Mark Gardner

  • Kaiju Boss Battle: A Dist::Zilla Journey from Chaos to Co-Op

    Kaiju Boss Battle: A Dist::Zilla Journey from Chaos to Co-Op

    Last week I wrote about devel­op­ing a Perl mod­ule enabling me to out­put log entries to macOS’ uni­fied log­ging sys­tem. This week­end’s adven­ture involved port­ing that mod­ule’s man­u­al process­es. These process­es includ­ed depen­den­cy man­age­ment, doc­u­men­ta­tion sync­ing, ver­sion bump­ing, and release. The goal was to make every­thing more auto­mat­ed and repeatable.

    And maybe even… monstrous.

    I was already famil­iar with the Dist::Zilla (DZil to its friends) suite of tools and plu­g­ins. I also knew that some Perl devel­op­ers view it as a huge bar­ri­er to entry. This per­cep­tion affects their will­ing­ness to con­tribute to oth­ers’ projects.

    So even though Log-​Any-​Adapter-​MacOS-​OSLog was a small mod­ule of inter­est to a lim­it­ed cod­ing audi­ence (macOS Perl users), I thought it wise to have both a main branch and sep­a­rate build/main branch for those pro­gram­mers who want­ed to work as though things had bare­ly changed:

    • Entire source code with full POD-​formatted documentation;
    • A Makefile.PL script to gen­er­ate a portable build­ing, test­ing, and installing Makefile;
    • Plus, every Perl dis­tri­b­u­tion should sup­ply the typ­i­cal README, MANIFEST, LICENSE, and CONTRIBUTING doc­u­men­ta­tion. This is essen­tial if it’s meant for pub­lic consumption.

    A small dis­tri­b­u­tion would also give me a mod­el I can scale up for larg­er projects. At the very least, it was anoth­er learn­ing oppor­tu­ni­ty for me.

    Boy, was it.

    Why Dist::Zilla?

    Because I know it can auto­mate away the boil­er­plate code and rep­e­ti­tion of infor­ma­tion that’s unfor­tu­nate­ly nec­es­sary in a mod­ern Perl mod­ule distribution:

    • the ver­sion num­bers in every mod­ule and script
    • the README that often includes the same intro­duc­to­ry text as the main mod­ule’s documentation
    • the nam­ing, order, and con­tent of POD sec­tions (via DZil’s sis­ter suite, Pod::Weaver), some of which repeat dis­tri­b­u­tion meta­da­ta like author, ver­sion, sup­port, copy­right, license, and so on

    If you need fur­ther detail, Dan Book’s Dist::Zilla::Starter is, as its name sug­gests. an excel­lent and remark­ably thor­ough guide to the how and why of Dist::Zilla. It even cov­ers the basic struc­ture of CPAN dis­tri­b­u­tions and the his­to­ry of Perl mod­ule author­ing tools.

    Why not something else?

    Because just as in the sto­ry of Goldilocks and the three bears, every­thing else I looked at seemed want­i­ng in some way:

    • Module::Build/​Module::Build::Tiny: Although min­i­mal, pure-​Perl, and easy to install, Module::Build prop­er still requires ship­ping extra boil­er­plate and dupli­cate meta­da­ta. ::Tiny shaves that down fur­ther but drops whole swaths of func­tion­al­i­ty. As an exam­ple, you can’t spec­i­fy at set­up time that users need a spe­cif­ic oper­at­ing sys­tem. It’s a deal-​breaker for a mod­ule that requires macOS 10.12 Sierra or newer.
    • Minilla: Opinionated con­ven­tion over con­fig­u­ra­tion, but I don’t share its opin­ions. Overriding direc­to­ry lay­out, test struc­ture, and README/​license han­dling would mean fight­ing Minilla’s defaults, DZil-​config style, with­out DZil’s plu­g­in ecosystem.
    • ShipIt: Simple, one-​file con­fig­u­ra­tion. But too sim­ple: it’s most­ly release automa­tion and not author­ing automa­tion. Everything I want­ed to tool away is still manual.
    • No or min­i­mal tool­chain: That was how I start­ed this mod­ule, with ExtUtils::MakeMaker. Totally man­u­al, with every con­trib­u­tor, includ­ing yours tru­ly, need­ing to remem­ber all the mov­ing parts by hand.

    Off to see the lizard!

    My DZil dist.ini con­fig­u­ra­tion file start­ed off sim­ply enough:

    name    = Log-Any-Adapter-MacOS-OSLog
    author  = Mark Gardner <[email protected]>
    license = Perl_5
    copyright_holder = Mark Gardner
    version = 0.0.5 ; bump as appropriate
    
    [MetaResources]
    repository.type = git
    bugtracker.web  = https://codeberg.org/mjgardner/perl-Log-Any-Adapter-MacOS-OSLog/issues
    
    [Repository]
    web = https://codeberg.org/mjgardner/perl-Log-Any-Adapter-MacOS-OSLog

    That [MetaResources] is the first evi­dence of using plu­g­ins. It pro­vides a nice tidy way to spec­i­fy the meta­da­ta. Automated tools can use this infor­ma­tion to index, exam­ine, pack­age, or install Perl distributions.

    So far so good. But then the mon­ster attacked.

    [@Filter]
    -bundle = @Basic
    -remove = GatherDir
    -remove = MakeMaker
    -remove = Readme
    ; License plugin still active here
    
    [GatherDir]
    include_dotfiles = 1
    exclude_filename = .gitignore
    exclude_filename = .build
    
    [ReadmeAnyFromPod]
    type = markdown
    filename = README.md
    location = build
    
    [CopyFilesFromBuild]
    copy = README.md
    
    ; various MakeMaker::*, Git::*, Pod::Weaver, etc. plugins

    I was get­ting errors from the dzil --build com­mand as it attempt­ed to add the same file mul­ti­ple times. These were gen­er­at­ed files com­mit­ted to the main ver­sion con­trol branch and a build/main branch for non-​DZil contributors.

    This brought me to a dead stop. I went down a rabbit-​hole exam­in­ing built files from rsync(1) with [Run::AfterBuild]., com­par­ing them to the branch I had set up.

    Plus my .perlcriticrc file was dis­ap­pear­ing from the build arti­facts despite that instruc­tion to [GatherDir] to include dot­files.

    “Let them fight.”

    LICENSE to kill (files)

    Eventually I traced the LICENSE file dupli­ca­tion to duel­ing DZil plu­g­ins. The afore­men­tioned [GatherDir] was duti­ful­ly copy­ing the file from my work­ing root even as the [License] plu­g­in was gen­er­at­ing it. I did­n’t want to lose the lat­ter source of what­ev­er license I hap­pened to be using to dis­trib­ute this code.

    And .perlcriticrc? It turns out the [PruneCruft] plu­g­in does­n’t lis­ten to its friend [GatherDir] and was hoover­ing it away.

    In the end, I had to expand my manually-​configured plu­g­in ros­ter a lit­tle to keep them from fight­ing over what files came from where:

    [@Filter]
    -bundle = @Basic
    -remove = GatherDir
    -remove = PruneCruft
    -remove = MakeMaker
    -remove = Readme
    
    [PruneCruft]
    except = .perlcriticrc
    
    [GatherDir]
    include_dotfiles = 1
    exclude_filename = .gitignore
    exclude_filename = .build
    exclude_filename = LICENSE
    
    [ReadmeAnyFromPod]
    type            = markdown
    filename        = README.md
    
    [CopyFilesFromBuild]
    copy = README.md
    copy = LICENSE

    With these tweaks to Log-​Any-​Adapter-​MacOS-​OSLog’s dist.ini:

    • [License] gen­er­ates its file,
    • [CopyFilesFromBuild] copies it along with the README into the root for my commit,
    • And [GatherDir] does­n’t stomp on it like so many Tokyo city blocks.

    The build/main branch for non-​DZil con­trib­u­tors would con­tain exact­ly what they and I expect­ed. It would be a match of the DZil work­ing copy plus arti­facts, git cloneable with no surprises.

    Lessons learned, and what’s next?

    I want­ed to serve two styles of devel­op­ment and had signed myself up for a com­pli­cat­ed author­ing process. I now have more knowl­edge of which plu­g­in runs in each phase of the build. This allows me to decide between gen­er­at­ed and version-​controlled files.

    CPAN has a rich vari­ety of per­son­al Dist::Zilla::PluginBundle::s fac­tor­ing indi­vid­ual authors’ pref­er­ences into a sin­gle place. Those authors don’t have to copy-​and-​paste DZil con­fig­u­ra­tions around. It’s past time for me to mint one of my own bun­dles for more con­sis­tent and well-​understood Perl dis­tri­b­u­tions. Then I can start spin­ning up new projects with­out revis­it­ing the same pain.


    This week­end has brought on a men­tal shift. I moved from fight­ing DZil’s defaults to mak­ing it work my way. I also found sat­is­fac­tion in a pre­dictable, minimal-​effort Perl release pipeline.

    The mon­ster was just a guy in a rub­ber suit all along.

  • Logging from Perl to macOS’ unified log with FFI and Log::Any

    Logging from Perl to macOS’ unified log with FFI and Log::Any

    Part 1: The elephant in the room

    A few weeks ago, I start­ed host­ing my own Mastodon instance on a Mac mini in my home office. I want­ed to join the social Fediverse on my own terms–but it did­n’t take long to notice bal­loon­ing disk usage. Cached media from oth­er users’ posts was pil­ing up fast.

    That got me think­ing: how do I track this growth before it gets out of hand?

    Logging seemed like the obvi­ous answer. On Unix and Linux sys­tems, it’s straight­for­ward enough. But on macOS, find­ing a native, main­tain­able solu­tion takes more digging.

    Part 2: Feeding the Apple

    macOS is Unix-​based, so you’d expect log­ging to be sim­ple. You can install logro­tate via Homebrew, then sched­ule it with cron(8). It works–but it adds lay­ers of con­fig­u­ra­tion files, per­mis­sions, and guess­work. I want­ed some­thing native. Something that felt like it belonged on a Mac.

    Turns out, macOS offers two built-​in options. One is newsys­log, a BSD-​style tool that rotates logs based on size or time. It’s reli­able, but it requires priv­i­leged root-owned con­fig­u­ra­tion files and feels like a holdover from old­er Unix systems.

    The oth­er is Apple’s uni­fied log­ging sys­tem–a mod­ern API used across macOS, iOS, and even watchOS. It’s struc­tured, search­able, and already baked into the plat­form. That’s the one I decid­ed to explore.

    Howard Oakley’s explain­er on the Unified Log helped me under­stand Apple’s sys­tem for con­sol­i­dat­ing logs. It showed how they are stored in a com­pressed bina­ry for­mat, com­plete with struc­tured meta­da­ta and pri­va­cy con­trols. With that foun­da­tion, I turned to Apple’s OSLog Framework doc­u­men­ta­tion. It showed how to tag entries and fil­ter them with pred­i­cates. macOS han­dles the rest.

    It’s elegant–but you need to use the API to write logs. Yes, read­ing and fil­ter­ing can be done on the com­mand line or in the Console app. But Apple seems to expect log­ging to be the sole province of Swift and Objective‑C devel­op­ers. I’d rather not have to learn a new pro­gram­ming lan­guage just to write logs.

    UPDATE: Howard Oakley’s blow­hole util­i­ty pro­vides a sim­ple way to write to the uni­fied log from the com­mand line, but all mes­sages come from the ​“co.eclecticlight.blowhole” sub­sys­tem with a ​“gen­er­al” cat­e­go­ry. We can do better.

    Part 3: A platypus in the key of C

    I do know Perl. I also know just enough C to be dan­ger­ous. And I briefly con­sid­ered learn­ing Swift or Objective‑C. Nevertheless, I won­dered about bridg­ing Perl to Apple’s uni­fied log­ging sys­tem with­out switch­ing languages.

    macOS expos­es a C API in <os/log.h>:

    #include <os/log.h>
    
    void
    os_log(os_log_t log, const char *format, ...);
    
    void
    os_log_info(os_log_t log, const char *format, ...);
    
    void
    os_log_debug(os_log_t log, const char *format, ...);
    
    void
    os_log_error(os_log_t log, const char *format, ...);
    
    void
    os_log_fault(os_log_t log, const char *format, ...);

    Perl’s CPAN has a mod­ule called FFI::Platypus that would let me call for­eign func­tions in C and oth­er lan­guages. It looked promising.

    But there’s a catch: these log­ging func­tions are vari­adic macros, not plain func­tions. That makes them inac­ces­si­ble via FFI. Worse, they expand into pri­vate API calls–unstable across OS updates and risky to rely upon.

    So I wrote a small C wrap­per to con­vert each macro into a prop­er func­tion. This makes them FFI-​safe and lets me con­trol vis­i­bil­i­ty (pub­lic log­ging vs. pri­vate, redact­ed log­ging) using Apple’s for­mat specifiers:

    #include <os/log.h>
    
    #define DEFINE_OSLOG_WRAPPERS(level_macro, suffix)    \
        void os_log_##suffix##_public(os_log_t log,       \
                                      const char *msg) {  \
            level_macro(log, "%{public}s", msg);          \
        }                                                 \
        void os_log_##suffix##_private(os_log_t log,      \
                                       const char *msg) { \
            level_macro(log, "%{private}s", msg);         \
        }
    
    // Generate wrappers for each log level
    DEFINE_OSLOG_WRAPPERS(os_log, default)
    DEFINE_OSLOG_WRAPPERS(os_log_info, info)
    DEFINE_OSLOG_WRAPPERS(os_log_debug, debug)
    DEFINE_OSLOG_WRAPPERS(os_log_error, error)
    DEFINE_OSLOG_WRAPPERS(os_log_fault, fault)

    This macro gen­er­ates two func­tions per log level–one pub­lic, one private–giving down­stream Perl code a choice. It’s ver­bose, but it’s safe, auditable, and future-proof.

    Part 4: Plugging into Log::Any

    With the wrap­per library in place, I began map­ping Apple’s log lev­els to some­thing Perl can use. I chose Log::Any from CPAN because it’s light­weight, wide­ly sup­port­ed, and its adapters don’t lock you into a spe­cif­ic back-​end. The same code that logs to the screen can also log to a file, or in our case, Apple’s system.

    Admittedly, at this point I’m no longer writ­ing a sim­ple log­ging script for my Mastodon instance. Instead, it’s a full-​fledged log­ging mod­ule. Oh well.

    Some Log::Any lev­els share the same under­ly­ing Apple call– OSLog does­n’t dis­tin­guish between notice and info or trace and debug. That’s a lit­tle dif­fer­ent from how Unix sys­log does things, but that’s fine. The goal here is com­pat­i­bil­i­ty, not per­fect fidelity.

    Building a sim­ple dis­patch table to route log mes­sages based on lev­el, I then used FFI::Platypus to bind each wrap­per function:

    use FFI::Platypus 2.00;
    
    my %OS_LOG_MAP = (
        trace     => 'os_log_debug',
        debug     => 'os_log_debug',
        info      => 'os_log_info',
        notice    => 'os_log_info',
        warning   => 'os_log_fault',
        error     => 'os_log_error',
        critical  => 'os_log_default',
        alert     => 'os_log_default',
        emergency => 'os_log_default',
    );
    
    my $ffi = FFI::Platypus->new(
        api => 2,
        lib => [ './liboslogwrapper.dylib' ],
    );
    
    $ffi->attach(
        [ os_log_create => '_os_log_create' ],
        [ 'string', 'string' ],
        'opaque',
    );
    
    # attach each wrapper function
    my %UNIQUE_OS_LOG = map { $_ => 1 } values %OS_LOG_MAP;
    foreach my $function ( keys %UNIQUE_OS_LOG ) {
        for my $variant (qw(public private)) {
            my $name = "${function}_$variant";
            $ffi->attach(
                [ $name => "_$name" ],
                [ 'opaque', 'string' ],
                'void',
            );
        }
    }

    This set­up gives me a clean way to log from Perl using Apple’s native sys­tem. I can achieve this with­out touch­ing Swift, Objective‑C, or exter­nal tools. Each log lev­el maps to a C wrap­per, and the FFI lay­er han­dles the rest.

    Now I just need an init func­tion to cre­ate the os_​log_​t object and a set of meth­ods for log­ging and detect­ing whether a giv­en log lev­el is enabled:

    use strict;
    use Carp;
    use base qw(Log::Any::Adapter::Base);
    use Log::Any::Adapter::Util qw(
      detection_methods
      numeric_level
    );
    
    sub init {
        my $self = shift;
        $self->{private} ||= 0;
        croak 'subsystem is required'
          unless defined $self->{subsystem};
    
        $self->{_os_log} = _os_log_create(
          @{$self}{qw(subsystem category)},
        );
    
        return;
    }
    
    foreach my $log_level ( keys %OS_LOG_MAP ) {
        no strict 'refs';
        *{$log_level} = sub {
            my ( $self, $message ) = @_;
    
            &{  "_$OS_LOG_MAP{$log_level}_"
                    . ( $self->{private}
                        ? 'private'
                        : 'public'
                    ) }( $self->{_os_log}, $message );
        };
    }
    
    foreach my $method ( detection_methods() ) {
        my $method_level = numeric_level(substr $method 3);
        no strict 'refs';
        *{$method} = sub {
            !!( $method_level <= (
              $_[0]->{log_level} // numeric_level('info')
            ) );
        };
    }

    What’s that ​“sub­sys­tem” bit up there? That’s the term macOS uses for iden­ti­fy­ing process­es in logs. They’re usu­al­ly for­mat­ted in reverse DNS nota­tion (e.g., ​“com.example.perl”). Once again, Howard Oakley has a great explain­er on the top­ic.

    Also, there’s some metapro­gram­ming going on there:

    • The first fore­ach loop cre­ates func­tions called trace, debug, and info. These func­tions call the cor­re­spond­ing FFI::Platypus-created func­tions. It uses the pri­vate vari­ants if the pri­vate attribute for the log adapter was set.
    • The sec­ond fore­ach loop cre­ates cre­ates func­tions called is_​trace, is_​debug, is_​info, etc., that return true if the adapter is catch­ing that lev­el of log message.

    Part 5: At long last, logging… mostly

    Once this is pack­aged in a Perl mod­ule, how do you use it? At least that part isn’t too hard:

    use Log::Any '$log', default_adapter => [
      'MacOS::OSLog', subsystem => 'com.phoenixtrap.perl',
    ];
    use English;
    use Carp qw(longmess);
    
    $log->info('Hello from Perl!');
    $log->infof('You are using Perl %s', $PERL_VERSION);
    
    $log->trace( longmess('tracing!') );
    $log->debug(     'debugging!'     );
    $log->info(      'informing!'     );
    $log->notice(    'noticing!'      );
    $log->warning(   'warning!'       );
    $log->error(     'erring!'        );
    $log->critical(  'critiquing!'    );
    $log->alert(     'alerting!'      );
    $log->emergency( 'emerging!'      );

    And then you can run this com­mand line to stream log mes­sages from the sub­sytem used above:

    % log stream --level debug \
      --predicate 'subsystem == "com.phoenixtrap.perl"

    What hap­pened to the trace and debug log mes­sages that were sup­posed to call os_log_debug(3)? According to macOS’ log(1) man­u­al page, you have to explic­it­ly allow debug­ging out­put for a giv­en subsystem:

    % sudo log config --mode "level:debug" \
      --subsystem com.phoenixtrap.perl

    Et voilà!

    Hmm, same lack of debug­ging messages.

    I’m still fig­ur­ing this out. Any clues? Drop me a line!

    UPDATE: This is now fixed thanks to some inspi­ra­tion from the source code of Log::Any::Adapter::Syslog. I’ve updat­ed the code on Codeberg; here is the diff.

    Bonus: Fancy output

    Thanks to Log::Any::Proxy, you also get sprintf for­mat­ting vari­ant functions:

    use English;
    $log->infof(
        'You are using Perl %s in %d',
        $PERL_VERSION, (localtime)[5] + 1900,
    );
    You are using Perl v5.40.2 in 2025

    If you out­put an object that over­loads string rep­re­sen­ta­tion, you get that string:

    use DateTime;
    $log->infof('It is now %s', DateTime->now);
    It is now 2025-08-10T20:16:50

    And you get single-​line Data::Dumper out­put of com­plex data struc­tures, plus replac­ing unde­fined val­ues with the string ​“undef”:

    $log->info( {
        foo    => 'hello',
        bar    => 'world',
        colors => [ qw(
            red
            green
            blue
        ) ],
        null => undef,
    } );
    {bar => "world",colors => ["red","green","blue"],foo => "hello",null => undef}

    Conclusion: Build once, use everywhere

    The best tools aren’t always the ones you planned to build. They’re the ones that solve a prob­lem cleanly–and then solve five more you hadn’t thought of yet.

    What start­ed as a quick fix for Mastodon media mon­i­tor­ing became a reusable bridge between Perl and macOS’ Unified Log. Along the way, I got to explore Apple’s log­ging inter­nals, write an FFI-​respecting C wrap­per, and inte­grate clean­ly with Log::Any. The result­ing code is mod­u­lar, auditable, and–most importantly–maintainable.

    I did­n’t set out to write a log­ging adapter. But when you care about clean ops and repro­ducible infra­struc­ture, some­times the best tools are the ones you build your­self. And if they hap­pen to be over-​engineered for the task at hand? All the better–they’ll prob­a­bly out­live it.

    Try it out or contribute!

    The full adapter code is on Codeberg. If you’re log­ging from Perl on macOS, give it a spin. Contributions, bug reports, and real-​world feed­back are welcome–especially if you’re test­ing it in pro­duc­tion or on old­er macOS versions.

    I’ll do my best to stay com­pat­i­ble with past and future macOS and Perl releas­es. Keeping the code auditable and min­i­mal should help it stay use­ful with­out becom­ing a mov­ing target.

  • Lightweight object-​oriented Perl scripts: From modulinos to moodulinos

    Lightweight object-​oriented Perl scripts: From modulinos to moodulinos

    Last week I found myself devel­op­ing a Perl script to cat­a­log some infor­ma­tion for our qual­i­ty assur­ance team. Unfortunately, as these things some­times do, the scrip­t’s com­plex­i­ty and require­ments start­ed increas­ing. I still want­ed to keep it as a sim­ple script. Yet, it was grow­ing com­mand line argu­ments that need­ed extra val­i­da­tion. I also need­ed to test some func­tions with­out wait­ing for the entire script to run.

    As with many things Perl, the basic solu­tion is fair­ly old. Over twen­ty years ago, bri­an d foy pop­u­lar­ized the mod­uli­no pat­tern. in which Perl scripts that you exe­cute from the com­mand line can also act as Perl mod­ules. You can even use these mod­ules in oth­er con­texts, for exam­ple test­ing.* A mod­uli­no seemed like the per­fect solu­tion for test­ing indi­vid­ual script func­tions, but writ­ing object-​oriented Perl out­side of a frame­work (or the new Perl class syn­tax) can be chal­leng­ing and verbose.

    Enter the cow (Moo)

    The Moo sys­tem of mod­ules are billed as a light­weight way ​“to con­cise­ly define objects and roles with a con­ve­nient syn­tax that avoids the details of Perl’s object sys­tem.” It does­n’t have any XS code. Thus, it does­n’t need a C com­pil­er to install. Unlike its inspi­ra­tion, Moose, it’s opti­mized for the fast start­up time need­ed for a command-​line script. Sure, you don’t get a full-​strength meta-​object pro­to­col for query­ing and manip­u­lat­ing class­es, objects, and attributes—those capa­bil­i­ties are con­cerns for larg­er appli­ca­tions or libraries. In keep­ing with the light­weight theme, you can use Type::Tiny con­straints for para­me­ter val­i­da­tion. Additionally, there are sev­er­al solu­tions for turn­ing command-​line argu­ments into object attrib­ut­es. (I chose to use MooX::Options, main­ly because of its easy avail­abil­i­ty as an Ubuntu Linux pack­age.)

    I’m not about to dump a pro­pri­etary script here on my blog. Yet, I have worked up an illus­tra­tive exam­ple of how to incor­po­rate Moo into a mod­uli­no. Call it a ​“mooduli­no” if you like; here’s a short-​ish script to tell Perl just how you feel at this time of day:

    #!/usr/bin/env perl
    
    use v5.38;
    
    package moodulino;
    use Moo;
    use MooX::Options;
    use Types::Standard qw(ArrayRef Str);
    
    option name => (
        is       => 'ro',
        isa      => Str,
        required => 1,
        short    => 'n',
        doc      => 'your name here',
        format   => 's',
    );
    
    option moods => (
        is        => 'ro',
        isa       => ArrayRef [Str],
        predicate => 1,
        short     => 'm',
        doc       => 'a list of how you might feel',
        format    => 's@',
        autosplit => ',',
    );
    
    has time_of_day => (
        is      => 'ro',
        isa     => Str,
        builder => 1,
    );
    
    sub _build_time_of_day ($self) {
        my %hours = (
             5 => ‘morning’,
            12 => 'afternoon',
            17 => 'evening',
            21 => 'night',
        );
    
        for ( sort { $b <=> $a } keys %hours ) {
            return $hours{$_} if (localtime)[2] >= $_;
        }
        return 'night';
    }
    
    sub run ($self) {
        printf "Good %s, %s!\n",
          $self->time_of_day,
          $self->name;
    
        if ( $self->has_moods ) {
            say 'How are you feeling?';
            say "- $_?" for $self->moods->@*;
        }
    }
    
    package main;
    
    main() unless caller;
    
    sub main { moodulino->new_with_options->run() }

    And here’s what hap­pens when I run it:

    % chmod a+x moodulino.pm
    % ./moodulino.pm
    name is missing
    USAGE: moodulino.pm [-h] [long options ...]
    
        -m --moods=[Strings]  a list of how you might feel
        -n --name=String      your name here
    
        --usage               show a short help message
        -h                    show a compact help message
        --help                show a long help message
        --man                 show the manual
    % ./moodulino.pm --name Mark
    Good afternoon, Mark!
    % ./moodulino.pm —name Mark --moods happy --moods sad --moods excited
    Good afternoon, Mark!
    How are you feeling?
    - happy?
    - sad?
    - excited?
    % ./moodulino.pm —name Mark --moods happy,sad,excited
    Good afternoon, Mark!
    How are you feeling?
    - happy?
    - sad?
    - excited?

    If the mood strikes, I can even write a test script for my script:

    #!/usr/bin/env perl
    
    use v5.38;
    use Test2::V0;
    use moodulino;
    
    plan(3);
    
    my $mood = moodulino->new( name => 'Bessy' );
    isa_ok( $mood, 'moodulino' );
    can_ok( $mood, 'time_of_day' );
    
    is( $mood->time_of_day,
        in_set( qw(
            morning
            afternoon
            evening
            night
        ) ) );

    And run it:

    % prove -I. t/time_of_day.t
    t/daytime.t .. ok
    All tests successful.
    Files=1, Tests=3,  0 wallclock secs ( 0.00 usr  0.00 sys +  0.07 cusr  0.01 csys =  0.08 CPU)
    Result: PASS

    * foy lat­er expand­ed this idea into the chap­ter ​“Modules as Programs” in Mastering Perl (2007). You can also read more in his 2014 arti­cle ​“Rescue lega­cy code with mod­uli­nos”. Also explore Gábor Szabó’s arti­cles on the top­ic. ↩︎

  • Watch Ayn Rand: A Sense of Life for free

    Do you have a pub­lic library card or go to a uni­ver­si­ty? Then you can watch the Oscar-​nominated doc­u­men­tary Ayn Rand: A Sense of Life for free on Kanopy!

    The film sweeps from Rand’s child­hood and escape from Soviet Russia through her strug­gles in Hollywood to her even­tu­al tri­umph as the best­selling author of The Fountainhead and Atlas Shrugged. These books sell hun­dreds of thou­sands of copies annu­al­ly after over half a century.

  • Tony Levin and Stick Men

    Tony Levin is far and away my favorite musi­cian. Even before I picked up the bass gui­tar, I kept find­ing his name in the lin­er notes of my most-​liked albums. I’ve seen him play with Peter Gabriel, King Crimson, Stick Men, and with his broth­er Pete play­ing in their Levin Brothers jazz combo.

    And of course, once I did start study­ing his bass (and Chapman Stick) lines, they were a rev­e­la­tion. Endlessly cre­ative, both dri­ving and being dri­ven by the song, only showy when the moment called for it, flu­id, some­times fierce, always the per­fect mix­ture of tech­nique and emotion.

    I’m due to see Stick Men when they swing down to Houston in two months. Until then, here’s their lat­est EP: