Beefy Boxes and Bandwidth Generously Provided by pair Networks Frank
Syntactic Confectionery Delight
 
PerlMonks  

Don’t Repeat Your… version number

by Aristotle (Chancellor)
on Jul 20, 2006 at 23:01 UTC ( #562735=perlmeditation: print w/ replies, xml ) Need Help??

The other day, BooK posted the following to the module-authors mailing list:

[Y]ou could also simply expose the information in the documentation, and fetch it from there: (a trick I discovered thanks to Abigail’s additions to Acme::MetaSyntactic, see the upcoming Acme::MetaSyntactic::tour_de_france for an example):

my %const = map { s/\s+//; $_ } map { split /\s*=>\s*/ } grep { /=>/ } map { split /\n/ } << '=cut'; =pod This module uses the following constants: bang_eth => 1 biff => 2 krunch => 3 =cut

Now, that in itself is a damn cool hack.

But it immediately set my mind thinking about how to use it for the one thing that always annoys me about module maintenance: updating the module version in both your POD and on the $VERSION line.

Turns out, this is actually very tricky because you have to get the polyglot understood by three different tools:

  1. perl, which uses a very simple rule for what it regards as non-POD. Easy – see above.

  2. POD formatters, which use even simpler rules for what they regard as POD. No problems here, and it’s what makes Abigail’s trick possible.

  3. ExtUtils::MakeMaker – or to be precise, its MM->parse_version method –, which is what a lot of modules use to extract version information from modules. Oh dear.

    It skips POD using… shall we say, simplistic rules, much like POD formatters, so it will tend to successfully ignore precisely the things that a POD formatter will accept. It will also accept only a single line, which will be eval’ed in isolation.

In other words, something like this, which was my first thought, won’t work:

$VERSION = ( <<'=cut' =~ /\b\d+\.\d+\b/ ); =head1 VERSION This document describes Some::Module 0.1 =cut

If you try that, you will find that parse_version will eval just this:

$VERSION = ( <<'=cut' =~ /\b\d+\.\d+\b/ );

Useless.

I had to resort to treachery: reading MakeMaker’s source to find its weaknesses. And the weakness, it turns out, is that it uses /^=cut/ to stop skipping. Notice something? That matches too many things… Gotcha! You’re going down.

Unfortunately, the single-line requirement means that the version number must be on the same line as the string $VERSION, which means we’ll have to have that in the POD:

eval "package Some::Module; $_" for grep m/ = /, split /\n/, <<'=cut'; =head1 VERSION =for fooling makemaker =cut-feigned This document describes Some::Module, $VERSION = 0.1 =cut

Here, the heredoc operator on the first line sets perl up to treat the entire following section as a string. In that string we look for a line with an equals operator, then eval it.

The =for line makes POD formatters ignore what’s on the next line, unless one of them thinks it’s the formatter for the output format called “fooling”, for which a formatter is unlikely to ever be written.

And what’s on the next line, the =cut-feigned, makes parse_version stop skipping and look for a line which sets $VERSION.

It works:

$ perldoc ./Some/Module.pm | grep VERSION This document describes Some::Module, $VERSION = 0.1 $ perl -MSome::Module \ -le'print Some::Module->VERSION' 0.1 $ perl -MExtUtils::MakeMaker \ -le'print MM->parse_version(shift)' Some/Module.pm 0.1

Would I use this is actual CPAN-published code? I don’t know. But you have to admit, it is really quite a fun hack.

Makeshifts last the longest.

Comment on Don’t Repeat Your… version number
Select or Download Code
Re: Don’t Repeat Your… version number
by xdg (Monsignor) on Jul 21, 2006 at 02:17 UTC

    Wow. ++ just for figuring all that out.

    Personally, I'd worry that depending on parsers to handle the feigned cut is a bit risky.

    But, I like the principle of not repeating the version number -- I always used to forget to update the Pod as well as the code. That's why I hacked some simple variable substitution into Pod::WikiDoc:

    =begin wikidoc = NAME Pod::WikiDoc - Generate Pod from inline wiki style text = VERSION This documentation refers to version %%VERSION%%. =end wikidoc

    I use a custom Build.PL to automatically fill that in when the Pod is pre-processed. (See Pod::WikiDoc::Cookbook).

    -xdg

    Code written by xdg and posted on PerlMonks is public domain. It is provided as is with no warranties, express or implied, of any kind. Posted code may not have been tested. Use of posted code is at your own risk.

      FWIW, my module Devel::Required takes a similar approach to automatically update version information for required external modules. I guess it could be easily expanded to include the version information of the module itself.

      Liz

        That would be grand. The less boilerplate I need to deploy, the better.

        Makeshifts last the longest.

Re: Don't Repeat Your version number
by tinita (Parson) on Jul 21, 2006 at 15:12 UTC
    i also thought about that, and in the past i have tried several techniques. i found that it all seems to be too much work. i'm now using:
    my $version_pod = <<'=cut'; =pod =head1 NAME Foo.:Bar - blah =head1 VERSION $VERSION = "0.71" =cut our $VERSION = "0.71"; ... sub __test_version { my $v = __PACKAGE__->VERSION; return 1 if $version_pod =~ m/VERSION.*\Q$v/; return; }
    and then i have a test that calls the __test_version() method.
    this way i have to type the version twice, but hey, that's ok. the point is, i can't forget it as my testsuite would fail.

    additionally, Test::Pod fails with your hack. =)

      That’s a nice idea. Complete automation would be better, but if the computer traps forgetfulness, that’s almost as good.

      Makeshifts last the longest.

Re: Don’t Repeat Your… version number
by Herkum (Parson) on Jul 21, 2006 at 19:15 UTC

    I can say I was not so clever as you were. I made a script to be used when I bundled my distributions together. It would read the MANIFEST for files in the lib/ directory. From those files it would do a search and replace for,

    our $VERSION = '(\d\.\d{2})';
    and for POD,
    This documentation refers to B\<.+\> version (\d\.\d{2})

    (I am sure that this not 100% correct, I am basing it off-memory).

    The script allowed me to enter one value and then update the version number for all the files in my distribution. It got tedious to update 20+ .pm and .pod files all the time.

      Yeah, that’s roughly xdg’s approach above, just a little less neat because it’s not integrated with the rest of the tools.

      Makeshifts last the longest.

Re: Don’t Repeat Your… version number
by sciurius (Novice) on Jul 21, 2006 at 20:48 UTC
    Extremely nifty, but there's one big BUT... It works due to what I'd call a buglet in EU::MM. As soon as that gets fixed, the show is over. So I wouldn't use it in published code.

      It’s just generally brittle. It’s not just EU::MM getting fixed that would break it. Apparently all the POD-aware tools have different parsing quirks: tinita mentioned above that Test::Pod barfs on it, and apparently Module::Build does too (each for different reasons).

      So no, it’s not ending up in my CPAN code. But it made for an intriguing diversion for half an hour, so I’m still satisfied.

      Makeshifts last the longest.

Re: Don’t Repeat Your… version number
by Anonymous Monk on Jul 25, 2006 at 12:37 UTC
    Yes "VERSION" has to be in the POD. But it has not to be visible. The line consists actually of three parts, that commonally fall together, but need not. First the declaration: * or $ followed by any name that ends in "VERSION", second the activation consisting of a '=',which doesn't have to do anything with the declared variable, and third the perl code that assigns to the declared variable. This code can be anything, that fits on the line. Before and after these three parts can be anything, that makes sense, if compiled as perl. For instance
    This document applies to Version " X<< ";$myVERSION=join("",map{substr$_,0,-4+length}substr(q+ >>0.1.2X +<< +,3)).join"",map{""}q< < >> "

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlmeditation [id://562735]
Approved by Hue-Bond
Front-paged by broquaint
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others pondering the Monastery: (5)
As of 2014-04-21 11:32 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    April first is:







    Results (493 votes), past polls