Tommy has asked for the wisdom of the Perl Monks concerning the following question:

The Problem

Let's say you have a need to find out the version of a given module on your system. Easy right? Usually I just do this:

perl -MSome::Module -e 'print Some::Module->VERSION'

However I ran into some problems with that today. Have you ever tried that little trick above? It seems slick right? I've seen it used in various documentation before, but with it's "accuracy", it suffers a serious shortcoming: it requires the module to be compiled by the Perl interpreter and anything in a BEGIN block gets *Gasp!* executed.

The Trouble

That's just fine until you try that one-liner on, for example, Test::More or others like it. Many, many, many modules in the Test::* namespace start getting crazy kinds of busy in just the compile-time phase. They start testing schtuff. And they start spewing all kinds of lines of output that have nothing to do with the version number you were looking for. In fact, you might get such mangled output that you never see the version number at all because the module thought it was use()d incorrectly and bailed before "run time" ever kicked off. We could complain about that, but there's no point in it. The behavior is annoying in this case, but it had to be so. By design, these modules are working just as they should, and you can't very well change that.

The Compromise

So what's next? While this is a bit of an annoyance, you could somewhat easily search out the source code for a given module on your system and open it up (Perl is open source!) Then you have the version number right there and all is well. You got what you came for.

The Fun Begins

But wait cowboy! Not so fast! What if you need to automate this? What if you need to find out the version number of many modules, maybe upwards of 20 or more, and you might need to do this on a regular basis? The solution seems that you're going to have to downgrade your expectations a bit and try to parse out the version numbers of a given large list of modules using some as-intelligent-as-possible regexes.

The downside to this is that you're trying to determine with the greatest level of certainty that you're regex(es) are actually going to find the *correct* module version number. And while it's ugly, it's not impossible for that kind of information to be obscured in the module source by an author who was trying to be "cute" or "clever" or even downright devious. It is even perfectly legal for a module to change it's supposed $VERSION number, treating it as any other mutable variable. It can be done at compile time and runtime too (anybody who does that should get a really long timeout in the corner). That original one-liner I put out there has it's weakness, but herein lies its strength: when it runs, it sees in real time what the real module version is supposed to be. This kind of goes without saying, but I thought I'd highlight it anyway for the sake of completeness.

In the end, unless you want to write a full-blown script using PPI (which I tried and it actually misses things that grep does not), or unless you know something I don't, you're left with the regex option. And that's not fun.

The Challenge

I'm going to show you how I did it, and I'm going to ask you if you know a better way-- becuase I'd like to use a better way than what I've currently got. So let's move forward and consider a more specific use case: You've got a directory of files that contain Perl source code and they could contain any random number of use statements. Just assume that they are using all kinds of modules. Your job is to detect those "use" statements, and find out what the running $VERSION number is for each module getting use()d.

The perldoc utility provides an awesome feature that let's you specify the -l (lowercase L) flag, and perldoc will print out the location of the source file for that module. Very handy. Knowing this, you can ask perldoc for the location of each file for each "use" statement you detect. The rest of the business of discovering the true version of that module is up to you.

Here's how I did it today, and here's exactly what I hope you can help me improve:

The Code

# at the command prompt, assuming we're already in the top-level direc +tory # containing all the perl source files... Be advised: this won't work + until # you remove all the comments grep -rhP '\buse\b' * | \ # find "use"-like statements perl -E 'say $_ =~ /\buse\s+([\w:]+)/ for <>;' | \ # find the modules +being used sort | \ # weed out... uniq | \ # ...any duplicates while read packname; # and start looping through the results do perldoc -l $packname 2>&-; # ask perldoc where source file is, igno +re errors done | \ grep -v '.pod$' | \ # exclude pod files from the results; they are not + modules while read packfile; # and loop through the results of the source file + lookups do echo -n $packfile ' -> '; # here the real output begins. Show the +filename: grep -P '\$([\w:]){0,}VERSION\b\s+=\s{0,}[[:punct:]]{0,1}\d' $packf +ile; # then ^^above^^ try your best to parse out the version number of t +he module # which will then get sent to the terminal done

The "clean" version (that does work when executed)

grep -rhP '\buse\b' * | \ perl -E 'say $_ =~ /\buse\s+([\w:]+)/ for <>;' | \ sort | \ uniq | \ while read packname; do perldoc -l $packname 2>&-; done | \ grep -v '.pod$' | \ while read packfile; do echo -n $packfile ' -> '; grep -P '\$([\w:]){0,}VERSION\b\s+=\s{0,}[[:punct:]]{0,1}\d' $packf +ile; done

...And finally, the actual one-liner in all it's beauty

grep -rhP '\buse\b' * | perl -E 'say $_ =~ /\buse\s+([\w:]+)/ for <>;' + | sort | uniq | while read packname; do perldoc -l $packname 2>&-; d +one | grep -v '.pod$' | while read packfile; do echo -n $packfile ' - +> '; grep -P '\$([\w:]){0,}VERSION\b\s+=\s{0,}[[:punct:]]{0,1}\d' $pa +ckfile; done

The Output

I piped the previous command through | column -ts - ...

/usr/share/perl/5.10/ > our $VERSI +ON = '1.04'; /usr/share/perl/5.10/File/ > our $VERSI +ON = '1.14'; /usr/lib/perl/5.10/File/ > $VERSION = + '3.40'; /usr/share/perl/5.10/File/ > $VERSION = + '0.22'; /usr/share/perl/5.10/ > $VERSION = + "1.50"; /usr/local/share/perl/5.10.1/Module/ > $VERSION = + '0.4003'; /usr/local/share/perl/5.10.1/Pod/Coverage/ > $Pod::Co +verage::TrustPod::VERSION = '0.100002'; /usr/share/perl/5.10/ > our $VERSI +ON = '1.00'; /usr/local/share/perl/5.10.1/Test/CPAN/ > our $VERSI +ON = '0.19'; /usr/local/share/perl/5.10.1/Test/CPAN/ > $VERSION = + '0.22'; /usr/local/share/perl/5.10.1/Test/CPAN/Meta/ > $VERSION = + '0.14'; /usr/local/share/perl/5.10.1/Test/ > $Test::D +istManifest::VERSION = '1.012'; /usr/local/share/perl/5.10.1/Test/ > $Test::F +atal::VERSION = '0.010'; /usr/local/share/perl/5.10.1/Test/ > $VERSION = + '1.01'; /usr/local/share/perl/5.10.1/Test/ > $Test::M +inimumVersion::VERSION = '0.101080'; /usr/local/share/perl/5.10.1/Test/ > our $VERSI +ON = '0.7'; # VERSION /usr/local/share/perl/5.10.1/Test/ > our $VERSI +ON = '0.98'; /usr/local/share/perl/5.10.1/Test/ > $VERSION = + '1.3'; /usr/local/share/perl/5.10.1/Test/ > $VERSI +ON = '1.04'; /usr/local/share/perl/5.10.1/Test/Perl/ > our $VERSI +ON = 1.02; /usr/local/share/perl/5.10.1/Test/ > our $VERSI +ON = '1.45'; /usr/local/share/perl/5.10.1/Test/Pod/ > our $VERSI +ON = "1.08"; /usr/local/share/perl/5.10.1/Test/Portability/ > $Test::P +ortability::Files::VERSION = '0.06'; /usr/local/share/perl/5.10.1/Test/ > $VERSI +ON = '1.07'; /usr/local/share/perl/5.10.1/Test/ > our $VERSI +ON = '0.002'; /usr/local/share/perl/5.10.1/Test/ > our $VERSI +ON = '1.002001'; # VERSION

The Bottom Line

So there you have it. That's an ugly, but working one-liner that produces only a few false-positives which can be easily weeded out or the regexes improved. Can you show me up and do it better (please)?


It CAN be improved. Here's a step in the right direction. When I have more time I'll move it into that one-liner! One might consider it cheating, but it isn't cheating any more than using perldoc -l

perl -MCPAN -MCPAN::Shell -e 'CPAN::Shell->autobundle'| awk '{ print $1 " " $2 }' | column -t ... now read that into a hash and pull out the versions you need from the modules you found in the first bit of the grep command I presented.

A mistake can be valuable or costly, depending on how faithfully you pursue correction