File Coverage

blib/lib/Module/Husbandry.pm
Criterion Covered Total %
statement 101 536 18.8
branch 35 332 10.5
condition 3 88 3.4
subroutine 16 49 32.6
pod 18 19 94.7
total 173 1024 16.8


line stmt bran cond sub pod time code
1             package Module::Husbandry;
2              
3             $VERSION = 0.002;
4              
5 1     1   5471 use Exporter;
  1         1  
  1         140  
6             @ISA = qw( Exporter );
7             @EXPORT_OK = qw(
8             cppm
9             install_file
10             mvpm
11             newpm
12             newpmbin
13             newpmdist
14             parse_cli
15             parse_module_specs
16             parse_dist_specs
17             reconfigure_dist
18             rmpm
19             skeleton_files
20             test_scripts_for
21             templates_for
22             usage
23             );
24             %EXPORT_TAGS = ( all => \@EXPORT_OK );
25              
26              
27             =head1 NAME
28              
29             Module::Husbandry - build and manage perl modules in a Perl module distribution
30              
31             =head1 SYNOPSIS
32              
33             =head1 DESCRIPTION
34              
35             =head1 FUNCTION
36              
37             =over
38              
39             =cut
40              
41 1     1   5 use strict;
  1         2  
  1         40  
42 1     1   2882 use Fatal qw( mkdir close );
  1         15849  
  1         6  
43 1     1   963 use File::Basename;
  1         2  
  1         2673  
44              
45             sub _x { ## "eXception"
46 0 0   0   0 my $options = ref $_[-1] ? pop : {};
47              
48 0 0       0 if ( $options->{describe} ) {
49 0         0 warn @_;
50             }
51             else {
52 0         0 die @_;
53             }
54             }
55              
56             sub _d { ## "describe". Return 1 if in describe-only mode.
57 0     0   0 my $options = pop;
58 0         0 my $msg = join "", @_;
59 0         0 1 while chomp $msg;
60 0 0       0 if ( defined $options->{_prog_name} ) {
61 0   0     0 my $sep = $options->{_prog_name_sep} || ":";
62 0         0 $sep .= " ";
63 0 0       0 $sep = "" if $msg =~ /^\W/;
64 0         0 $msg =~ s/^/$options->{_prog_name}$sep/gm;
65             }
66 0         0 $msg .= "\n";
67 0 0       0 print $msg unless $options->{quiet};
68 0         0 $options->{describe};
69             }
70              
71              
72             sub _d_c { # "describe command".
73 0     0   0 my $options = $_[-1];
74 0         0 local $options->{_prog_name_sep} = '$';
75 0         0 _d @_;
76             }
77              
78             sub _rel($) {
79 0     0   0 my ( $p ) = @_;
80 0         0 require File::Spec;
81 0         0 $p = File::Spec->abs2rel( $p );
82             }
83              
84              
85             sub _mkdir {
86 0     0   0 my ( $dir, $options ) = @_;
87 0 0 0     0 unless ( -d $dir or _d_c "mkdir -p ", _rel $dir, $options ) {
88 0         0 require File::Path;
89 0         0 File::Path::mkpath( [ $dir ] );
90             }
91             }
92              
93              
94             sub _mkparentdir {
95 0     0   0 my ( $fn, $options ) = @_;
96 0         0 _mkdir( (fileparse $fn)[1], $options );
97             }
98              
99              
100             sub _chdir {
101 0     0   0 my ( $dir, $options ) = @_;
102 0         0 require Cwd;
103 0 0       0 return if $dir eq Cwd::cwd();
104 0         0 _mkdir $dir, $options;
105 0 0 0     0 chdir $dir or die "$!: $dir\n"
106             unless _d_c "chdir ", _rel $dir, $options;
107             }
108            
109              
110             =item parse_module_specs
111              
112             my @specs = parse_module_specs @ARGV, \%options;
113              
114             Parses a module specification, one of:
115              
116             Foo
117             Foo::Bar
118             lib/Foo.pm
119             lib/Foo/Bar.pm
120             lib/Foo/Bar.pod
121              
122             and returns the package name (C) and the path to the
123             file (C) for each parameter in a hash. The result HASHes
124             look like:
125              
126             {
127             Filename => "lib/Foo/Bar.pm",
128             Package => "Foo::Bar",
129             Spec => $spec, ## What was passed in
130             };
131              
132             Any name containing characters other that A-Z, 0-9, :, or ' are assumed
133             to be filenames. Filenames should begin with lib/ (or LIB/ on Win32)
134             or will be warned about.
135              
136             The only option provided is:
137              
138             as_dir Set this to 1 to suppress the add "/" instead of ".pm"
139             the Filename when a module name is converted to a filename.
140             Does not affect anything when a filename is parsed. This
141             is used by mvpm's recurse option.
142              
143             =cut
144              
145             sub parse_module_specs {
146 13 100 66 13 1 1574 my $options = @_ && ref $_[-1] ? pop : {};
147              
148 16         22 map {
149 13         22 my $spec = $_;
150              
151             my ( $pkg, $fn ) = $spec =~ /[^\w:']/
152             ? do {
153 6         33 require File::Spec;
154 6         31 my $p = File::Spec->canonpath( $spec );
155 6         12 for ( $p ) {
156 6         10 s{^(\.[\\/]+)+}{};
157 6 50       40 ( $^O =~ /Win32/
    50          
158             ? s{^lib[\\/]+}{}i
159             : s{^lib[\\/]+}{}
160             ) or warn "Module spec '$spec' does not begin with lib/\n";
161 6         13 s{[\\/]+}{::}g;
162 6         29 s{\..*\z}{};
163             }
164 6         13 ( $p, $spec );
165             }
166 16 100       46 : do {
167 10         20 ( my $p = $spec ) =~ s{::}{/}g;
168 10 100       37 ( $spec, $options->{as_dir} ? "lib/$p" : "lib/$p.pm" );
169             };
170              
171             {
172 16         97 Filename => $fn,
173             Package => $pkg,
174             Spec => $spec,
175             };
176             } @_;
177             }
178              
179              
180             =item parse_bin_specs
181              
182             my @specs = parse_bin_specs @ARGV, \%options;
183              
184             Parses specifications for a "bin" program, like:
185              
186             foo
187             bin/foo
188              
189             and returns the program name (C) and the path to the
190             file (C) for each parameter in a hash. The result HASHes
191             look like:
192              
193             {
194             Filename => "bin/foo",
195             Program => "foo",
196             Spec => $spec, ## What was passed in
197             };
198              
199             If a spec has no directory separators, "bin/" is prepended.
200             If a spec has directory separator, no "bin/" is prepended.
201              
202             =cut
203              
204             sub parse_bin_specs {
205 0 0 0 0 1 0 my $options = @_ && ref $_[-1] ? pop : {};
206              
207 0         0 map {
208 0         0 my $spec = $_;
209              
210 0         0 require File::Spec;
211 0         0 my @names = File::Spec->splitdir( $spec );
212 0 0       0 unshift @names, "bin" if @names == 1;
213 0         0 my $fn = File::Spec->canonpath( File::Spec->catdir( @names ) );
214 0         0 my $program = (fileparse $fn);
215              
216             {
217 0         0 Filename => $fn,
218             Program => $program,
219             Spec => $spec,
220             };
221             } @_;
222             }
223              
224              
225             =item parse_dist_specs
226              
227             Takes a list of distributions specs (Foo::Bar, Foo-Bar) and returns a hash
228             like
229              
230             {
231             Package => "Foo::Bar",
232             Spec => $spec,
233             }
234              
235             =cut
236              
237             sub parse_dist_specs {
238 5         9 map {
239 4     4 1 251 my $spec = $_;
240              
241 5         11 ( my $pkg = $spec ) =~ s{-}{::}g;
242 5         9 ( my $dn = $spec ) =~ s{::}{-}g;
243              
244             {
245 5         27 Spec => $spec,
246             Package => $pkg,
247             DistName => $dn,
248             };
249             } @_;
250             }
251              
252              
253             =item reconfigure_dist
254              
255             Runs perl Makefile.PL using the current Perl.
256              
257             TODO: Support Module::Build methodology.
258              
259             =cut
260              
261             sub reconfigure_dist {
262 0     0 1 0 my ( $options ) = @_;
263              
264 0 0       0 if ( -f "Makefile" ) {
    0          
265 0 0       0 if ( -f "Makefile.PL" ) {
266 0 0       0 unless ( _d_c "touch Makefile.PL", $options ) {
267 0         0 my $time = time;
268 0 0       0 utime $time, $time, "Makefile.PL"
269             or warn "$! touching Makefile.PL";
270 0 0 0     0 if ( (stat "Makefile")[9] >= $time
271             && ! _d_c "untouch Makefile", $options
272             ) {
273 0 0       0 utime $time - 1, $time - 1, "Makefile"
274             or warn "$! touching Makefile.PL";
275             }
276             }
277             }
278              
279 0 0       0 system "make Makefile" unless _d_c "make Makefile\n", $options;
280             }
281             elsif ( -f "Makefile.PL" ) {
282 0 0       0 system $^X, "Makefile.PL" unless _d_c "$^X Makefile\n", $options;
283             }
284             else {
285 0 0       0 warn "Can't reconfigure distribution, no Makefile or Makefile.PL found\n"
286             unless $options->{describe};
287             }
288              
289             }
290              
291              
292             =item add_to_MANIFEST
293              
294             add_to_MANIFEST "foo", "bar";
295              
296             Adds one or more files to the MANIFEST.
297              
298             =cut
299              
300             ## TODO: back up to the backup dirs used elsewhere.
301              
302             sub _backup_and_read_MANIFEST {
303 0 0 0 0   0 my $options = @_ && ref $_[-1] ? pop : {};
304              
305 0         0 my @manifest;
306 0 0 0     0 if ( -e "MANIFEST" && ! _d_c "cp MANIFEST MANIFEST.old", $options ) {
307 0 0       0 open MANIFEST, "
308             or die "$!: MANIFEST\n";
309 0         0 @manifest = grep length, map {
310 0         0 1 while chomp;
311 0         0 $_;
312             } ;
313 0         0 close MANIFEST;
314 0 0 0     0 unlink "MANIFEST.old" or die "$!: MANIFEST.old"
315             if -e "MANIFEST.old";
316 0 0       0 rename "MANIFEST", "MANIFEST.old"
317             or die "$! while renaming MANIFEST to MANIFEST.old\n";
318 0         0 $options->{clean_up_MANIFEST} = 1;
319             }
320              
321 0         0 return \@manifest;
322             }
323              
324             sub _write_MANIFEST {
325 0     0   0 my ( $manifest, $options ) = @_;
326              
327 0 0       0 unless ( $options->{describe} ) {
328             ## TODO: Also add other files not in MANIFEST.SKIP by default?
329             ## Normally, this is done by the skeleton MANIFEST.
330 0 0       0 push @$manifest, "MANIFEST" unless @$manifest;
331              
332 0 0       0 open MANIFEST, ">MANIFEST" or die "$!: MANIFEST";
333 0         0 my %seen;
334 0 0       0 print MANIFEST map "$_\n", sort grep !$seen{$_}++, @$manifest
335             or die "$! writing MANIFEST";
336 0         0 close MANIFEST;
337             }
338              
339 0 0 0     0 unlink "MANIFEST.old" or warn "$! MANIFEST.old\n"
      0        
340             if $options->{clean_up_MANIFEST} && ! _d_c "rm MANIFEST.old", $options;
341             }
342              
343              
344             sub add_to_MANIFEST {
345 0 0 0 0 1 0 my $options = @_ && ref $_[-1] ? pop : {};
346              
347 0         0 my $manifest = _backup_and_read_MANIFEST $options;
348 0 0       0 unless ( _d_c
349             "echo ",
350             join( " ", map "'$_'", @_ ),
351             " >> MANIFEST ## and sort it",
352             $options
353             ) {
354 0         0 push @$manifest, @_;
355             }
356              
357 0         0 _write_MANIFEST $manifest, $options;
358             }
359              
360              
361             =item rm_from_MANIFEST
362              
363             rm_from_MANIFEST "foo", "bar";
364              
365             Remove one or more files to the MANIFEST.
366              
367             =cut
368              
369             sub rm_from_MANIFEST {
370 0 0 0 0 1 0 my $options = @_ && ref $_[-1] ? pop : {};
371              
372 0         0 my $manifest = _backup_and_read_MANIFEST $options;
373              
374 0 0       0 unless ( _d_c
375             "cat MANIFEST.old | grep -v '",
376             join( "|", map "$_", @_ ),
377             "' >> MANIFEST",
378             $options
379             ) {
380 0         0 my %doomed = map { ( $_ => 1 ) } @_;
  0         0  
381 0         0 @$manifest = grep ! exists $doomed{$_}, @$manifest;
382             }
383              
384 0         0 _write_MANIFEST $manifest, $options;
385             }
386              
387              
388             =item install_file
389              
390             install_file $from_file_hash, $to_file_hash, \%macros;
391              
392             Locates the approptiate file in the .newpm directory and copies it,
393             instantiating any <%macros%> needed.
394              
395             Reads <%meta foo bar %> and <%meta foo=bar %> tags.
396              
397             Meta tags
398             =========
399             <%meta chmod 0755 %> chmod the resulting file (numeric only)
400              
401             Any unrecognized meta or macro tags are ignored with a warning.
402              
403             Adds file to MANIFEST.
404              
405             TODO: adapt to Module::Build's manifesting procedures.
406              
407             =cut
408              
409             sub install_file {
410 0 0   0 1 0 my $options = @_ > 3 ? pop : {};
411 0         0 my ( $from, $to, $macros ) = @_;
412              
413 0 0       0 return if _d_c "install ",
414             basename( $from->{Filename} ),
415             " $to->{Filename}\n", $options;
416              
417 0         0 _mkparentdir $to->{Filename}, $options;
418              
419 0 0       0 open F, $from->{Filename} or die "$!: $from->{Filename}\n";
420 0 0       0 open T, ">$to->{Filename}" or die "$!: $to->{Filename}\n";
421 0         0 my %meta;
422 0         0 while () {
423 0         0 for my $macro ( keys %$macros ) {
424 0         0 s/<%\s*$macro\s*%>/$macros->{$macro}/gi;
425             }
426             s{
427             <%\s*META\s*([a-z]\w+)\s*(?:=\s*)?(.*?)\s*%>
428             }{
429 0         0 $meta{lc $1} = $2;
430 0         0 "";
431 0         0 }geix;
432             warn "install: WARNING: macro $1 in $from->{Filename} line $. ignored.\n"
433 0         0 for /(<%.*?%>)/g;
434 0 0       0 print T $_ or die "$! writing to $to->{Filename}\n";
435             }
436 0         0 close F;
437 0         0 close T;
438              
439 0 0       0 if ( my $perms = delete $meta{chmod} ) {
440 0 0       0 unless ( _d_c "chmod $perms $to->{Filename}", $options ) {
441 0 0       0 $perms = oct $perms if substr( $perms, 0, 1 ) eq "0";
442 0 0       0 chmod $perms, $to->{Filename}
443             or warn "$! chmod( $perms )ing $to->{Filename}\n";
444             }
445             }
446              
447             warn "install: WARNING: ignoring META setting",
448             " $_ $meta{$_} in $from->{Filename}\n"
449 0         0 for sort keys %meta;
450              
451 0         0 add_to_MANIFEST $to->{Filename}, $options;
452             }
453              
454              
455             =item templates_for
456              
457             my @from_files = templates_for @to_files;
458              
459             Given a list of files to write to, find the appropriate source files.
460              
461             =cut
462              
463             {
464 1     1   7 use vars qw( $template_dir );
  1         1  
  1         1319  
465            
466             sub template_dir {
467 2 50   2 0 6 if ( ! defined $template_dir ) {
468 0 0       0 $template_dir = File::Spec->catdir(
    0          
469             $^O =~ /Win32/
470             ? "C:\\etc"
471             : $ENV{HOME},
472             $^O =~ /Win32/
473             ? "newpm"
474             : ".newpm"
475             );
476             }
477 2         37 $template_dir;
478             }
479             }
480            
481              
482             sub templates_for {
483 1     1 1 5 require File::Spec;
484 2         6 map {
485 1         3 my $fn = $_->{Filename};
486 2         16 $fn =~ s{\A[^.]*(\.|\z)}{Template$1};
487             {
488 2         6 Filename => File::Spec->catfile( template_dir, $fn ),
489             };
490             } @_;
491             }
492              
493             =item test_scripts_for
494              
495             my @test_scripts = test_scripts_for @modules;
496              
497             Returns test scripts for any .pm and .pl file in @modules:
498              
499             {
500             Filename => "t/Foo.t",
501             }
502              
503             where @modules is an array of HASHes returned by parse_module_specs.
504              
505             =cut
506              
507             sub test_scripts_for {
508             map {
509 5     5 1 10 ( $_->{Filename} =~
510             ( ( $^O =~ /Win32/ )
511             ? qr/\.p[ml]\z/i
512             : qr/\.p[ml]\z/
513             ) )
514 6 50       65 ? do {
    100          
515 5         14 ( my $fn = $_->{Package} ) =~ s{::}{-}g;
516              
517             {
518 5         29 Filename => "t/$fn.t",
519             };
520             }
521             : ();
522             } @_;
523             }
524              
525             =item skeleton_files
526              
527             my %skel_map = skeleton_files $target_dir;
528              
529             Returns a list of from/to files to install from the skeleton directory.
530              
531             =cut
532              
533             sub skeleton_files {
534 0     0 1 0 my ( $target_dir ) = @_;
535              
536 0         0 require File::Find;
537 0         0 require File::Spec;
538              
539 0         0 my $skel_dir = File::Spec->catdir( template_dir, "skel" );
540              
541 0 0       0 die "$skel_dir not found\n" unless -e $skel_dir;
542              
543 0         0 my @files;
544             File::Find::find(
545             {
546             wanted => sub {
547 0 0   0   0 return unless -f;
548 0         0 my $to_fn = File::Spec->abs2rel( $_, $skel_dir );
549 0         0 $to_fn =~ s{\A(\.[\\/]+)+}{}g;
550 0         0 my $from_fn = File::Spec->catfile( $skel_dir, $to_fn );
551 0         0 push @files, [
552             { # From file
553             Filename => $from_fn,
554             },
555             { # To file
556             Filename => $to_fn,
557             },
558             ];
559             },
560 0         0 no_chdir => 1,
561             },
562             $skel_dir
563             );
564              
565 0         0 return @files;
566             }
567              
568             =item cppm
569              
570             cppm $from, $to, \%options
571              
572             Copies a file in a distribution and a related test suite (if found).
573              
574             TODO: Don't rewrite changelogs. Not sure how best to recognize them; this
575             could be an option for the mythical .newpmrc.
576              
577             TODO: Make the filename substitutions patterns case insensitive on Win32?
578              
579             =cut
580              
581             sub cppm {
582 0 0 0 0 1 0 my $options = @_ && ref $_[-1] ? pop : {};
583              
584 0         0 my @copies;
585 0         0 require File::Find;
586 0         0 require File::Spec;
587              
588 0         0 my %substs; ## Strings to substitute as a result of the name change
589             my $substs_pat; ## The re that looks for things to substitute
590              
591 0 0       0 if ( $options->{recurse} ) {
592 0         0 die "Sorry, -r not implemented yet.\n";
593 0         0 my ( $from, $to ) = parse_module_specs @_, { as_dir => 1 };
594              
595             ### SET from_pat, to_name
596              
597 0         0 my ( $bn, $dn ) = fileparse $from->{Filename};
598             File::Find::find(
599             {
600             no_chdir => 1,
601             wanted => sub {
602 0     0   0 my $p = File::Spec->abs2rel( $_, $dn );
603             ## TODO
604             },
605             },
606 0         0 $dn
607             );
608             }
609             else {
610 0         0 my ( $from, $to ) = parse_module_specs @_;
611              
612 0 0       0 _x "$from->{Filename} not found\n", $options
613             unless -e $from->{Filename};
614              
615 0 0       0 _x "$from->{Filename} is not a file\n", $options
616             unless -f _;
617              
618 0 0       0 _x "$to->{Filename} exists (and is a directory), not copying module\n", $options
619             if -d $to->{Filename};
620              
621 0 0       0 _x "$to->{Filename} exists, not copying module\n", $options
622             if -e _;
623              
624 0         0 push @copies, [ $from, $to ];
625              
626 0         0 %substs = (
627             $from->{Package} => $to->{Package},
628             $from->{Filename} => $to->{Filename},
629             );
630              
631 0         0 my ( $test_script_from ) = test_scripts_for $from;
632 0         0 my ( $test_script_to ) = test_scripts_for $to;
633              
634 0 0 0     0 if (
      0        
635             $test_script_from
636             && $test_script_to
637             && -f $test_script_from->{Filename}
638             ) {
639 0         0 push @copies, [ $test_script_from, $test_script_to ];
640 0         0 $substs{$test_script_from->{Filename}}
641             = $test_script_to->{Filename};
642             }
643              
644 0         0 $substs_pat = join(
645             join( "|", map quotemeta, sort keys %substs ),
646             "\\b(",
647             ")\\b"
648             );
649 0         0 $substs_pat = qr/$substs_pat/;
650             }
651              
652 0         0 require File::Copy;
653              
654 0         0 my ( $from_w, $to_w ) = ( 0, 0 ); # for pretty-printing
655              
656 0         0 for ( @copies ) {
657 0         0 my ( $from, $to ) = @$_;
658 0 0       0 $from_w = length $from->{Filename}
659             if length $from->{Filename} > $from_w;
660 0 0       0 $to_w = length $to->{Filename}
661             if length $to->{Filename} > $to_w;
662             }
663              
664             {
665 0         0 my $f_w = ( 0, 0 );
  0         0  
666 0         0 for ( keys %substs ) {
667 0 0       0 $f_w = length $_ if length $_ > $f_w;
668             }
669 0         0 my $f = "# subst: %-${f_w}s => %s\n";
670 0         0 _d sprintf( $f, $_, $substs{$_} ), $options for sort keys %substs;
671             }
672              
673 0         0 for ( @copies ) {
674 0         0 my $from_fn = $_->[0]->{Filename};
675 0         0 my $to_fn = $_->[1]->{Filename};
676              
677 0 0       0 unless ( _d_c
678             sprintf( "munge %-${from_w}s > %s\n", $from_fn, $to_fn ),
679             $options
680             ) {
681 0 0       0 open FROM, "<$from_fn" or die "$!: $from_fn";
682 0 0       0 open TO, ">$to_fn" or die "$!: $to_fn";
683              
684 0         0 while () {
685 0         0 s/$substs_pat/$substs{$1}/sge;
  0         0  
686 0         0 print TO $_;
687             }
688              
689 0         0 close FROM;
690 0         0 close TO;
691             }
692              
693 0         0 add_to_MANIFEST $to_fn, $options;
694             }
695              
696 0         0 reconfigure_dist $options;
697             }
698              
699             =item newpm
700              
701             Create new modules in ./lib/... and, if it's a .pm module,
702             a test suite in ./t/...
703              
704             Does not build the make file.
705              
706             =cut
707              
708             {
709 1     1   6 use vars qw( $time );
  1         1  
  1         894  
710             sub _time {
711 0 0   0   0 $time = time unless defined $time;
712 0         0 $time;
713             }
714             }
715              
716              
717             sub _newpm_installs {
718 0 0   0   0 my $options = ref $_[-1] ? pop : {};
719 0         0 my @modules = parse_module_specs @_;
720              
721 0         0 my @errors;
722              
723             my @installs;
724              
725 0         0 for my $module ( @modules ) {
726 0         0 my %macros = (
727             PackageName => $module->{Package},
728             ModulePath => $module->{Filename},
729             Date => scalar localtime( _time ),
730             Year => 1900 + (localtime( _time ))[5],
731             );
732              
733 0 0       0 push @errors, "$module->{Filename} found, can't overwrite\n"
734             if -e $module->{Filename};
735              
736 0         0 my ( $template ) = templates_for $module;
737              
738 0         0 push @installs, [ $template, $module, \%macros ];
739              
740 0         0 my ( $test_script ) = test_scripts_for $module;
741              
742 0 0       0 if ( defined $test_script ) {
743 0         0 my ( $test_script_template ) = templates_for $test_script;
744 0 0       0 _x "$test_script->{Filename} found, can't overwrite.\n", $options
745             if -e $test_script->{Filename};
746 0         0 push @installs, [ $test_script_template, $test_script, \%macros ];
747             }
748             }
749 0 0       0 _x @errors, $options if @errors;
750              
751 0         0 @installs;
752             }
753              
754              
755             sub newpm {
756 0 0   0 1 0 my $options = ref $_[-1] ? pop : {};
757 0         0 install_file @$_, $options for _newpm_installs @_, $options;
758 0         0 reconfigure_dist $options;
759             }
760              
761             =item newpmbin
762              
763             Create new script files in bin/. Does not add a test script
764             (since there's no safe way to test an arbitrary program).
765              
766             =cut
767              
768             sub _newpmbin_installs {
769 0 0   0   0 my $options = ref $_[-1] ? pop : {};
770 0         0 my @programs = parse_bin_specs @_;
771              
772 0         0 my @errors;
773              
774             my @installs;
775              
776 0         0 for my $program ( @programs ) {
777 0         0 my %macros = (
778             ProgramName => $program->{Program},
779             ProgramPath => $program->{Filename},
780             Date => scalar localtime( _time ),
781             Year => 1900 + (localtime( _time ))[5],
782             );
783              
784 0 0       0 push @errors, "$program->{Filename} found, can't overwrite\n"
785             if -e $program->{Program};
786              
787 0         0 my ( $template ) = templates_for $program;
788              
789 0         0 push @installs, [ $template, $program, \%macros ];
790              
791             # my ( $test_script ) = test_scripts_for $program;
792             #
793             # if ( defined $test_script ) {
794             # my ( $test_script_template ) = templates_for $test_script;
795             # _x "$test_script->{Filename} found, can't overwrite.\n", $options
796             # if -e $test_script->{Filename};
797             # push @installs, [ $test_script_template, $test_script, \%macros ];
798             # }
799             }
800 0 0       0 _x @errors, $options if @errors;
801              
802 0         0 @installs;
803             }
804              
805              
806             sub newpmbin {
807 0 0   0 1 0 my $options = ref $_[-1] ? pop : {};
808 0         0 install_file @$_, $options for _newpmbin_installs @_, $options;
809 0         0 reconfigure_dist $options;
810             }
811              
812             =item newpmdist
813              
814             Create a new distribution in . and populate it from the skeleton
815             files. newpm() a new module.
816              
817             =cut
818              
819             sub newpmdist {
820 0 0   0 1 0 my $options = ref $_[-1] ? pop : {};
821 0         0 my @installs;
822              
823 0         0 for my $dist ( parse_dist_specs @_ ) {
824 0         0 my ( $module ) = parse_module_specs $dist->{Package};
825              
826 0         0 my %macros = (
827             PackageName => $dist->{Package},
828             ProgramName => $dist->{Program},
829             DistName => $dist->{DistName},
830             ModulePath => $module->{Filename},
831             Date => scalar localtime( _time ),
832             Year => 1900 + (localtime( _time ))[5],
833             );
834              
835 0         0 my @files = skeleton_files $dist->{DistName};
836              
837 0 0       0 _x "No skeleton files found for dist $dist->{Spec}\n", $options
838             unless @files;
839              
840 0         0 push @installs, $dist->{DistName};
841 0         0 push @installs, map [ @$_, \%macros ], sort @files;
842 0         0 push @installs, _newpm_installs $module->{Package};
843 0         0 push @installs, "reconfigure!";
844             }
845              
846 0         0 require Cwd;
847 0         0 my $d = Cwd::cwd();
848              
849 0         0 for ( @installs ) {
850 0 0       0 if ( ref $_ ) {
    0          
851 0         0 install_file @$_, $options;
852             }
853             elsif ( $_ eq "reconfigure!" ) {
854 0         0 reconfigure_dist $options;
855             }
856             else {
857 0         0 my $dir = File::Spec->catdir( $d, $_ );
858 0         0 _chdir $dir, $options;
859             }
860             }
861              
862 0         0 _chdir $d, $options;
863             }
864              
865             =item mvpm
866              
867             mvpm $from, $to, \%options
868              
869             Changes the name of a file in a distribution and all occurences of the
870             file's name (and, if applicable, package name) in it and in all other
871             files.
872              
873             A backup of any files changed is placed in .newpm/bak_0000 (where 0000
874             increments each time).
875              
876             TODO: some kind of locking so simultaneous mvpms don't happen to choose
877             the same backup directory name.
878              
879             TODO: Don't rewrite changelogs. Not sure how best to recognize them; this
880             could be an option for the mythical .newpmrc.
881              
882             TODO: Make the filename substitutions patterns case insensitive on Win32?
883              
884             =cut
885              
886             {
887 1     1   6 use vars qw( $workdir );
  1         2  
  1         4964  
888             sub _workdir {
889 0 0   0   0 $workdir = ".mvpm.d" unless defined $workdir;
890 0         0 $workdir;
891             }
892             }
893              
894              
895             sub _mk_bak_dir {
896 0     0   0 my $options = pop;
897              
898 0         0 my $wd = _workdir;
899              
900 0         0 require File::Spec;
901 0         0 my $max = 0;
902 0         0 for ( glob( "$wd/bak_*" ) ) {
903 0 0       0 /\bbak_(\d+)/ or warn "Unusual backup dir name: '$_'\n";
904 0   0     0 my $n = $1 || 0;
905 0 0       0 $max = $n if $n > $max;
906             }
907              
908 0         0 my $bd = sprintf "$wd/bak_%04d", $max + 1;
909              
910 0 0       0 die "BUG: trying to reuse backup dir $bd" if -e $bd;
911              
912 0         0 _mkdir $bd, $options;
913              
914 0         0 return $bd;
915             }
916              
917              
918             sub mvpm {
919 0 0 0 0 1 0 my $options = @_ && ref $_[-1] ? pop : {};
920              
921 0         0 my @moves;
922 0         0 require File::Find;
923 0         0 require File::Spec;
924              
925 0         0 my %substs; ## Strings to substitute as a result of the name change
926             my $substs_pat; ## The re that looks for things to substitute
927              
928 0 0       0 if ( $options->{recurse} ) {
929 0         0 die "Sorry, -r not implemented yet.\n";
930 0         0 my ( $from, $to ) = parse_module_specs @_, { as_dir => 1 };
931              
932             ### SET from_pat, to_name
933              
934 0         0 my ( $bn, $dn ) = fileparse $from->{Filename};
935             File::Find::find(
936             {
937             no_chdir => 1,
938             wanted => sub {
939 0     0   0 my $p = File::Spec->abs2rel( $_, $dn );
940             ## TODO
941             },
942             },
943 0         0 $dn
944             );
945             }
946             else {
947 0         0 my ( $from, $to ) = parse_module_specs @_;
948              
949 0 0       0 _x "$from->{Filename} not found\n", $options
950             unless -e $from->{Filename};
951              
952 0 0       0 _x "$from->{Filename} is not a file\n", $options
953             unless -f _;
954              
955 0 0       0 _x "$to->{Filename} exists (and is a directory), not moving module\n", $options
956             if -d $to->{Filename};
957              
958 0 0       0 _x "$to->{Filename} exists, not moving module\n", $options
959             if -e _;
960              
961 0         0 push @moves, [ $from, $to ];
962              
963 0         0 %substs = (
964             $from->{Package} => $to->{Package},
965             $from->{Filename} => $to->{Filename},
966             );
967              
968 0         0 my ( $test_script_from ) = test_scripts_for $from;
969 0         0 my ( $test_script_to ) = test_scripts_for $to;
970              
971 0 0 0     0 if (
      0        
972             $test_script_from
973             && $test_script_to
974             && -f $test_script_from->{Filename}
975             ) {
976 0         0 push @moves, [ $test_script_from, $test_script_to ];
977 0         0 $substs{$test_script_from->{Filename}}
978             = $test_script_to->{Filename};
979             }
980              
981 0         0 $substs_pat = join(
982             join( "|", map quotemeta, sort keys %substs ),
983             "\\b(",
984             ")\\b"
985             );
986 0         0 $substs_pat = qr/$substs_pat/;
987              
988 0         0 require Cwd;
989 0         0 my $cwd = Cwd::cwd();
990              
991             File::Find::find(
992             {
993             no_chdir => 1,
994             wanted => sub {
995 0     0   0 my $p = File::Spec->abs2rel( $_, $cwd );
996 0         0 my $is_d = -d;
997              
998 0   0     0 $File::Find::prune =
999             $p eq template_dir
1000             || $p eq _workdir
1001             || $p eq "blib"
1002             || $p eq "pm_to_blib"
1003             || $p =~ /^change/
1004             || ( $is_d && substr( $p, 0, 1 ) eq "." );
1005              
1006 0 0       0 if ( $File::Find::prune ) {
1007 0 0       0 _d "# ignoring $p", $is_d ? "/..." : (), $options;
1008 0         0 return;
1009             }
1010              
1011 0 0       0 return if $is_d;
1012              
1013 0 0       0 if ( -B ) {
1014 0         0 _d "# ignoring binary file $_", $options;
1015 0         0 return;
1016             }
1017              
1018 0 0 0     0 return if $p eq $from->{Filename}
      0        
1019             || (
1020             $test_script_from
1021             && $p eq $test_script_from->{Filename}
1022             );
1023              
1024 0 0       0 open FROM, "<$p" or die "$! while scanning $p\n";
1025 0         0 while () {
1026 0 0       0 if ( /$substs_pat/ ) {
1027 0         0 my $f = {
1028             Filename => $p,
1029             };
1030 0         0 push @moves, [ $f, $f ];
1031 0         0 last;
1032             }
1033             }
1034 0 0       0 close FROM or die "$! closing $p\n";
1035             },
1036             },
1037 0         0 "."
1038             );
1039             }
1040              
1041 0         0 require File::Copy;
1042              
1043 0         0 my $bak_dir = _mk_bak_dir $options;
1044              
1045 0         0 my ( $from_w, $to_w, $bak_w ) = ( 0, 0, 0 ); # for pretty-printing
1046              
1047 0         0 for ( @moves ) {
1048 0         0 my ( $from, $to ) = @$_;
1049 0         0 $from->{BakFilename}
1050             = File::Spec->catfile( $bak_dir, $from->{Filename} );
1051              
1052 0 0       0 $from_w = length $from->{Filename}
1053             if length $from->{Filename} > $from_w;
1054 0 0       0 $bak_w = length $from->{BakFilename}
1055             if length $from->{BakFilename} > $bak_w;
1056 0 0       0 $to_w = length $to->{Filename}
1057             if length $to->{Filename} > $to_w;
1058             }
1059              
1060 0         0 for ( @moves ) {
1061 0         0 my $from_fn = $_->[0]->{Filename};
1062 0         0 my $bak_fn = $_->[0]->{BakFilename};
1063              
1064 0         0 _mkparentdir(
1065             File::Spec->catdir( $bak_dir, $_->[0]->{Filename} ),
1066             $options
1067             );
1068              
1069 0 0 0     0 File::Copy::copy( $from_fn, $bak_fn )
1070             or die "$! copying $from_fn to $bak_fn\n"
1071             unless _d_c
1072             sprintf( "cp %-${from_w}s %s\n", $from_fn, $bak_fn ),
1073             $options
1074             }
1075              
1076             {
1077 0         0 my $f_w = ( 0, 0 );
  0         0  
1078 0         0 for ( keys %substs ) {
1079 0 0       0 $f_w = length $_ if length $_ > $f_w;
1080             }
1081 0         0 my $f = "# subst: %-${f_w}s => %s\n";
1082 0         0 _d sprintf( $f, $_, $substs{$_} ), $options for sort keys %substs;
1083             }
1084              
1085 0         0 for ( @moves ) {
1086 0         0 my $from_fn = $_->[0]->{Filename};
1087 0         0 my $bak_fn = $_->[0]->{BakFilename};
1088 0         0 my $to_fn = $_->[1]->{Filename};
1089              
1090 0 0       0 unless ( _d_c
1091             sprintf( "munge %-${bak_w}s > %s\n", $bak_fn, $to_fn ),
1092             $options
1093             ) {
1094 0 0       0 open BAK, "<$bak_fn" or die "$!: $bak_fn";
1095 0 0       0 open NEW, ">$to_fn" or die "$!: $to_fn";
1096              
1097 0         0 while () {
1098 0         0 s/$substs_pat/$substs{$1}/sge;
  0         0  
1099 0         0 print NEW $_;
1100             }
1101              
1102 0         0 close BAK;
1103 0         0 close NEW;
1104             }
1105              
1106 0 0 0     0 unlink $from_fn or die "$! unlinking $from_fn"
      0        
1107             if $from_fn ne $to_fn && ! _d_c "rm $from_fn\n", $options;
1108             }
1109              
1110 0         0 reconfigure_dist $options;
1111             }
1112              
1113             =item rmpm
1114              
1115             Removes any modules and tests named after a package (or module) name.
1116              
1117             Warns about any other files that refer to the doomed package.
1118              
1119             A backup is made in the backup directory (.mvpm/... for now, will change)..
1120              
1121             TODO: Allow a site-specific rm command to be used, like 'trash', so
1122             this command may be better integrated with a user's working environment.
1123             This will wait until we restructure the directories.
1124              
1125             =cut
1126              
1127             sub rmpm {
1128 0 0 0 0 1 0 my $options = @_ && ref $_[-1] ? pop : {};
1129              
1130 0         0 my @deletes; ## Those who are about to die, we salute you...
1131 0         0 require File::Find;
1132 0         0 require File::Spec;
1133              
1134 0         0 my %spoor; ## Strings to scan for before deleting
1135             my $spoor_pat; ## The re used to scan for %spoor
1136 0         0 my @spoor; ## filenames, line numbers and lines of spoor that
1137             ## will be left behind.
1138              
1139 0 0       0 if ( $options->{recurse} ) {
1140 0         0 die "Sorry, -r not implemented yet.\n";
1141 0         0 my @doomed = parse_module_specs @_, { as_dir => 1 };
1142              
1143             ### SET doomed_pat
1144             #
1145             # my ( $bn, $dn ) = fileparse $from->{Filename};
1146             # File::Find::find(
1147             # {
1148             # no_chdir => 1,
1149             # wanted => sub {
1150             # my $p = File::Spec->abs2rel( $_, $dn );
1151             # ## TODO
1152             # },
1153             # },
1154             # $dn
1155             # );
1156             }
1157             else {
1158 0         0 my @doomed = parse_module_specs @_;
1159              
1160 0         0 for my $doomed ( @doomed ) {
1161 0 0       0 _x "$doomed->{Filename} not found\n", $options
1162             unless -e $doomed->{Filename};
1163              
1164 0 0       0 _x "$doomed->{Filename} is not a file\n", $options
1165             unless -f _;
1166              
1167 0         0 push @deletes, [ $doomed ];
1168              
1169 0         0 %spoor = (
1170             $doomed->{Package} => undef,
1171             $doomed->{Filename} => undef,
1172             );
1173              
1174 0         0 my ( $doomed_test_script ) = test_scripts_for $doomed;
1175              
1176 0 0 0     0 if (
1177             $doomed_test_script
1178             && -f $doomed_test_script->{Filename}
1179             ) {
1180 0         0 push @deletes, [ $doomed_test_script ];
1181 0         0 $spoor{$doomed_test_script->{Filename}} = undef;
1182             }
1183             }
1184              
1185 0         0 $spoor_pat = join(
1186             join( "|", map quotemeta, sort keys %spoor),
1187             "\\b(",
1188             ")\\b"
1189             );
1190 0         0 my $spoor_pat_re = qr/$spoor_pat/;
1191              
1192 0         0 require Cwd;
1193 0         0 my $cwd = Cwd::cwd();
1194              
1195             ## TODO: generalize all or part of this so mvpm() and rmpm() can
1196             ## share it.
1197             File::Find::find(
1198             {
1199             no_chdir => 1,
1200             wanted => sub {
1201 0     0   0 my $p = File::Spec->abs2rel( $_, $cwd );
1202 0         0 my $is_d = -d;
1203              
1204 0   0     0 $File::Find::prune =
1205             $p eq template_dir
1206             || $p eq _workdir
1207             || $p eq "blib"
1208             || $p eq "pm_to_blib"
1209             || $p =~ /^change/
1210             || ( $is_d && substr( $p, 0, 1 ) eq "." );
1211              
1212 0 0       0 if ( $File::Find::prune ) {
1213 0 0       0 _d "# ignoring $p", $is_d ? "/..." : (), $options;
1214 0         0 return;
1215             }
1216              
1217 0 0       0 return if $is_d;
1218              
1219 0 0       0 if ( -B ) {
1220 0         0 _d "# ignoring binary file $_", $options;
1221 0         0 return;
1222             }
1223              
1224 0 0       0 return if grep $p eq $_->{Filename}, @doomed;
1225              
1226 0 0       0 open SURVIVORS, "<$p" or die "$! while scanning $p\n";
1227 0         0 while () {
1228 0 0       0 if ( /$spoor_pat_re/ ) {
1229 0         0 1 while chomp;
1230 0         0 push @spoor, [ $p, $., $_ ];
1231 0         0 last;
1232             }
1233             }
1234 0 0       0 close SURVIVORS or die "$! closing $p\n";
1235             },
1236             },
1237 0         0 "."
1238             );
1239             }
1240              
1241 0 0       0 if ( @spoor ) {
1242 0         0 my ( $sfn_w, $ln_w ) = ( 0, 0 );
1243 0         0 my @spoor_recs;
1244 0         0 for ( @spoor ) {
1245 0         0 local $_ = [ "$_->[0],", @{$_}[1,2]];
  0         0  
1246 0         0 push @spoor_recs, $_;
1247 0 0       0 $sfn_w = length $_->[0] if length $_->[0] > $sfn_w;
1248 0 0       0 $ln_w = length $_->[1] if length $_->[1] > $ln_w;
1249             }
1250              
1251 0         0 my $spoor_format = "%-${sfn_w}s %${ln_w}d: %s\n";
1252              
1253 0         0 _d_c "grep -r '$spoor_pat' .", $options;
1254 0         0 printf $spoor_format, @$_ for @spoor_recs;
1255             }
1256              
1257              
1258 0         0 my $bak_dir = _mk_bak_dir $options;
1259              
1260 0         0 my ( $from_w, $bak_w ) = ( 0, 0, 0 ); # for pretty-printing
1261              
1262 0         0 for ( @deletes ) {
1263 0         0 my ( $from ) = @$_;
1264 0         0 $from->{BakFilename}
1265             = File::Spec->catfile( $bak_dir, $from->{Filename} );
1266              
1267 0 0       0 $from_w = length $from->{Filename}
1268             if length $from->{Filename} > $from_w;
1269 0 0       0 $bak_w = length $from->{BakFilename}
1270             if length $from->{BakFilename} > $bak_w;
1271             }
1272              
1273 0         0 require File::Copy;
1274 0         0 for ( @deletes ) {
1275 0         0 my $from_fn = $_->[0]->{Filename};
1276 0         0 my $bak_fn = $_->[0]->{BakFilename};
1277              
1278 0         0 _mkparentdir(
1279             File::Spec->catdir( $bak_dir, $_->[0]->{Filename} ),
1280             $options
1281             );
1282              
1283 0 0 0     0 File::Copy::copy( $from_fn, $bak_fn )
1284             or die "$! copying $from_fn to $bak_fn\n"
1285             unless _d_c
1286             sprintf( "cp %-${from_w}s %s\n", $from_fn, $bak_fn ),
1287             $options
1288             }
1289              
1290 0         0 for ( @deletes ) {
1291 0         0 my $from_fn = $_->[0]->{Filename};
1292 0 0       0 unlink $from_fn or die "$! unlinking $from_fn";
1293 0         0 rm_from_MANIFEST $from_fn;
1294             }
1295              
1296 0         0 reconfigure_dist $options;
1297             }
1298              
1299             =item usage
1300              
1301             =cut
1302              
1303             sub usage {
1304 0     0 1 0 my ( $messages, $spec ) = @_;
1305 0         0 my $prog_name = basename $0;
1306              
1307 0 0       0 push @$messages, "\nSee $prog_name --help for details" if @$messages;
1308 0 0       0 my $message = join "\n", @$messages, @$messages ? ( "", "" ) : ();
1309              
1310 0         0 my $examples = $spec->{examples};
1311              
1312 0         0 my $desc;
1313 0 0       0 $desc = $spec->{description} if ! length $message;
1314 0   0     0 $desc ||= "";
1315              
1316 0         0 my $options =
1317             join "\n", map {
1318 0         0 my $name = join ", ", grep length, split /\|+/;
1319             my @desc =
1320             length $messages
1321             ? ()
1322 0 0       0 : do {
1323 0         0 my $desc = $spec->{$_};
1324 0         0 $desc =~ s/^(\w+:)?(\w+=)?\s*//;
1325 0         0 $desc =~ s/^/ /;
1326 0         0 1 while chomp $desc;
1327 0         0 "$desc\n";
1328             };
1329 0         0 ( $name, @desc );
1330             } grep /^-/, sort keys %$spec;
1331              
1332 0         0 1 while chomp $message;
1333 0         0 1 while chomp $examples;
1334 0         0 1 while chomp $desc;
1335              
1336 0         0 s/^/ /mg for ( grep length, $examples, $desc, $options );
1337 0 0       0 $message = "$message\n\n" if length $message;
1338 0 0       0 $examples = "Usage\n\n$examples\n\n" if length $examples;
1339 0 0       0 $options = "Options (may occur anywhere except after a '--')\n\n$options\n\n" if length $options;
1340 0 0       0 $desc = "Description\n\n$desc\n\n" if length $desc;
1341              
1342 0         0 my $usage = "$message$examples$options$desc";
1343 0         0 $usage =~ s/%p/$prog_name/g;
1344 0         0 print $usage;
1345              
1346 0 0       0 exit length $messages ? 1 : 0;
1347             }
1348              
1349              
1350             =item parse_cli
1351              
1352             my ( $options, @params ) = parse_options @ARGV, \%spec;
1353              
1354             Reads the command line and parses out the options and other parameters.
1355             Options may be intermixed with parameters.
1356              
1357             Options -h|-?|--help and -- do the normal things always.
1358              
1359             -n|--describe print out what *would* happen, but do nothing.
1360              
1361             =cut
1362              
1363             sub parse_cli {
1364 2     2 1 291 my ( $cli, $spec ) = @_;
1365              
1366 2         3 my ( %options, @params ); ## These shall be returned if all is ok.
1367              
1368 2         101 $options{_prog_name} = basename $0;
1369              
1370 2         3 my %options_spec;
1371             my @errors;
1372 0         0 my $check;
1373 0         0 my ( $min_params, $max_params );
1374 0         0 my $found_examples;
1375              
1376 2         5 $spec->{"-h|-?|--help"} = "Display full help";
1377 2         5 $spec->{"-n|--describe"} = "Describe what would happen without doing it";
1378 2         4 $spec->{"--"} = "Mark end of options";
1379              
1380 2         11 for ( keys %$spec ) {
1381 12         21 my $desc = $spec->{$_};
1382 12 100       41 if ( substr( $_, 0, 1 ) eq "-" ) {
    50          
    100          
    50          
    0          
1383 8         9 my $type = "flag";
1384 8 50       22 $type = $1 if $desc =~ s/^(\w+)://;
1385 8         10 my $canonical_spelling;
1386 8 50       18 $canonical_spelling = $1 if $desc =~ s/^(\w+)=//;
1387            
1388 8         35 my @spellings = split /\|/;
1389 8 50       20 unless ( defined $canonical_spelling ) {
1390 8         13 $canonical_spelling = $spellings[-1];
1391 8         27 $canonical_spelling =~ s/^-+//;
1392 8         15 $canonical_spelling =~ s/\W/_/g;
1393 8         15 $canonical_spelling =~ s/^(\d)/_$1/;
1394             }
1395              
1396             my $action =
1397 1     1   7 $type eq "flag" ? sub { $options{$canonical_spelling} = $desc }
1398 8 50       37 : do {
1399 0         0 push @errors, "Unrecognized option type '$type:'";
1400 0         0 next;
1401             };
1402              
1403             $options_spec{$_} = $action
1404 8         57 for @spellings;
1405             }
1406             elsif ( $_ eq "check" ) {
1407 0         0 $check = $desc;
1408             }
1409             elsif ( $_ eq "param_count" ) {
1410 2 50       5 if ( $desc =~ /\A(\d+)\.\.((?:\d+)?)\z/ ) {
1411 0         0 ( $min_params, $max_params) = ( $1, $2 );
1412 0 0       0 $max_params = 1_000_000_000 unless length $max_params;
1413             }
1414             else {
1415 2         7 ( $min_params, $max_params) = ( $desc, $desc );
1416             }
1417             }
1418             elsif ( $_ eq "examples" ) {
1419 2         28 $found_examples = 1;
1420             }
1421             elsif ( $_ eq "description" ) {
1422             ## ignore it, it's optional
1423             }
1424             else {
1425 0         0 push @errors, "unrecognized option spec key '$_'";
1426             }
1427             }
1428              
1429 2 50       10 push @errors, "examples missing from command line parsing spec"
1430             unless $found_examples;
1431              
1432 2         3 my @checks;
1433 2 50       6 if ( defined $min_params ) {
1434             push @checks, sub {
1435 2     2   2 pop;
1436 2 0       34 join "",
    0          
    0          
    0          
    50          
    50          
1437             @_ < $min_params
1438             ? (
1439             "missing parameter",
1440             $min_params - @_ > 1 ? "s" : (),
1441             ": expected ",
1442             $min_params != $max_params ? "at least " : (),
1443             $min_params,
1444             ", got ",
1445             scalar @_,
1446             )
1447             : @_ > $max_params
1448             ? (
1449             "extra parameter",
1450             @_ - $max_params > 1 ? "s" : (),
1451             ": expected ",
1452             $min_params != $max_params ? "at most " : (),
1453             $max_params,
1454             ", got ",
1455             scalar @_,
1456             )
1457             : ();
1458 2         11 };
1459             }
1460              
1461 2 50       13 push @checks, $check if $check;
1462              
1463 2 50       5 require Carp, Carp::croak( join "\n", @errors ) if @errors;
1464              
1465             $options_spec{"--"} =
1466 2     1   12 sub { push @params, splice @$cli; last };
  1         3  
  1         4  
1467              
1468             $options_spec{"-h"} =
1469             $options_spec{"--help"} =
1470             $options_spec{"-?"} =
1471 2     0   16 sub { usage [], $spec };
  0         0  
1472              
1473 2         12 while ( @$cli ) {
1474 4         8 my $p = shift @$cli;
1475 4 100       11 if ( substr( $p, 0, 1 ) eq "-" ) {
1476 2         4 my $d = $options_spec{$p};
1477 2 50       6 unless ( defined $d ) {
1478 0         0 push @errors, "unrecognized option: $p";
1479 0         0 next;
1480             }
1481              
1482 2 50       6 if ( ref $d eq "CODE" ) {
1483 2 50       5 last unless defined $d->();
1484             }
1485             else {
1486 0         0 require Carp;
1487 0         0 Carp::confess "BUG: $d is not a CODE ref";
1488             }
1489             }
1490             else {
1491 2         7 push @params, $p;
1492             }
1493             }
1494              
1495             push @errors, grep defined && length, $_->( @params, \%options )
1496 2   33     9 for @checks;
1497              
1498 2 50       7 usage \@errors, $spec
1499             if @errors;
1500              
1501 2         31 return ( @params, \%options );
1502             }
1503              
1504             =back
1505              
1506             =head1 LIMITATIONS
1507              
1508             ASSumes a dir tree and file naming conventions like:
1509              
1510             Foo-Bar/
1511             Makefile.PL
1512             ...
1513             lib/Foo/Bar.pm
1514             t/Foo-Bar.pm
1515              
1516             This probably won't work out all that well for XS distributions, not
1517             sure how they work. Let me know and we'll see if we can add it :)
1518              
1519             Not tested on Win32.
1520              
1521             Does not know about Module::Build.
1522              
1523             Does not use anything like a .newpmrc file.
1524              
1525             =head1 COPYRIGHT
1526              
1527             Copyright 2002, R. Barrie Slaymaker, Jr., All Rights Reserved
1528              
1529             =head1 LICENSE
1530              
1531             You may use this module under the terms of the BSD, Artistic, oir GPL licenses,
1532             any version.
1533              
1534             =head1 AUTHOR
1535              
1536             Barrie Slaymaker
1537              
1538             =cut
1539              
1540             1;