I have been writing scripts for my employer for almost two years now, and, as I suspect is true of many of us, accumulating a documentation problem in the process. Specifically, I have found that it is much easier to add command-line options through class inheritance and reuse than it is to document them. I have attached an early version of the source for a module that tries to address the problem by inheriting the documentation in parallel with the option parsing code. For want of anything better, it's called "Pod::CLOD", for "Command Line Option Documenter". The internal POD (although incomplete) contains more details; the "HISTORY" section in particular says a bit more about how this whole mess came about.
For those curious enough to delve into it, I would be grateful for solutions to anything in the "BUGS" section (for those bugs that need more than just finishing up), or answers to the following questions in particular: 1. Has anybody seen or done anything similar? (I gave CPAN a glance, but there were no obvious candidates -- which probably just means that everybody is writing Web or GUI apps these days.) 2. Any suggestions for how to solve the delegation problem? Maybe Pod::CLOD should be allowed to do option processing for delegated objects directly? Or maybe it just needs hints about how precedence should work locally for a given module? Implementation is proving a bit trickier than I had expected -- the devil, as usual, is in the details -- but I'm pretty strongly motivated to make it work, since I don't really have a palatable alternative. And if it eventually becomes capable of meeting my needs, then I would be delighted to publish it via CPAN. Many TIA, -- Bob Rogers http://rgrjr.dyndns.org/
# Command Line Option Documenter. # # Copyright (C) 2004 by Bob Rogers <[EMAIL PROTECTED]>, all rights # reserved. The Pod::CLOD module is free software; you can redistribute it # and/or modify it under the same terms as Perl itself. # # [created. -- rgr, 4-Sep-04.] # # $Id:$ package Pod::CLOD; =head1 NAME Pod::CLOD -- The "Command Line Option Documenter" for Perl's "Plain Old Documentation." =head1 SYNOPSIS In module <Foo.pm>: package Foo; =head1 Option documentation =item B<--this> Specifies the C<--this> option. =item B<--that> Specifies the C<--that> option. [Foo edition.] =cut sub command_line_options { my $self = shift; # command_line_options must return a list of alternating # keywords and values, which must be acceptable to Getopt::Long. # But note that Pod::CLOD treats the option parsers (the subs # in this case) as black boxes. ('this=i' => sub { $self->this($_[1]); }, 'that=s' => sub { $self->that($_[1]); }); } . . . In module <Bar.pm>: package Foo; =head1 Option documentation =item B<--that> Specifies a different version of the C<--that> option. [Bar edition.] =cut sub command_line_options { my $self = shift; ('that=s' => sub { $self->i_resent_that($_[1]); }); } . . . In program C<main.pl>: use Foo; use Bar; use Pod::CLOD; my $foo = new Foo(); my $bar = new Bar(); my @loci; my $clod = new Pod::CLOD # $bar must come first here, so that its '--that' option overrides. (option_definers => [$bar, $foo], additional_options => { 'theother' => [EMAIL PROTECTED] }); $clod->parse_options(); . . . =head1 NAME main.pl -- just a random example. =head1 OPTIONS main.pl takes just the --this, --that, and --theother options: =over 4 =enumerate_options =back =head1 Option documentation Note that this text (until the next =head? section or =cut) does not appear in the final POD. Options handled by the main program could also be put after =enumerate_options above, but this (a) includes them in alphabetical order, (b) excludes them if the option is not actually used, and (c) ensures that the main program's documentation for these options overrides any module documentation. =item B<--theother> Specifies the C<--theother> option. . . . =cut =head1 REQUIRES Getopt::Long Pod::Usage =head1 DESCRIPTION C<Pod::CLOD> provides an object-oriented interface to C<Getopt::Long> and C<Pod::Usage>, handling command-line option parsing and providing the C<--help>, C<--usage>, and C<--man> options natively. Options from objects (or classes) that support the C<command_line_options> method can also be included. Furthermore, option documentation within such modules can be interpolated into a script's POD before processing by C<Pod::Usage>. C<Pod::CLOD> traces the C<@ISA> graph to find the right documentation (usually) for a given module, allowing scripts to share the documentation for their common options that are implemented by the same class. =head2 History Once upon a time, I started writing a set of interrelated scripts that used C<Getopt::Long> to parse command-line options, and C<Pod::Usage> to report problems with those options. Each script would do its own option parsing, then initialize one or more objects using the options it was given, and finally compute a result somehow. Since some objects were used frequently, there was a great deal of duplicated option parsing code, so I soon started delegating option parsing to the C<command_line_options> method of each class that actually used the option values. This scaled admirably well, as class C<command_line_options> methods could compose option handling lists from superclasses as well as from delegated objects. (I restricted the return value representation to alternating keyword/value pairs to facilitate just this sort of processing.) Unfortunately, each script still had to contain the complete documentation for each option it accepted, directly or through its constituent classes. When I made a 40-line change to the code of one popular module to add a feature that rendered 400 lines of POD out-of-date at a stroke (roughly ten lines of documentation change in each of 43 scripts), I knew I had to find a way to document options that was as modular as the code itself. In hindsight, of course, the answer was obvious: Inherit the POD along with the options. Each module can document the options it provides, making it easier to change the option documentation when the option code is changed. Subclass modules that modify how an option is used can also modify the documentation (whether or not they modify how the option is parsed). And if an option is modified by deleting it, its documentation is suppressed altogether. Unfortunately, there are still some snags in the implementation . . . =head1 BUGS [These are all fairly serious problems, and explain why this module is not ready for prime time yet. -- rgr, 7-Sep-04.] C<foo=s> will not override C<foo=i>; they are seen as distinct options. Along the same vein, "short form" options and aliases are not handled at all. There is no way to document options defined by delegation. Mistakes can be made even for options defined through the class hierarchy. If in the example above, Bar suppressed C<--that> instead of redefining it, but some later option definer provided its own C<--that>, C<Pod::CLOD> would incorrectly use Bar's documentation for it. Suppression of "Option documentation" sections in main scripts is not yet implemented. Handling of boolean options, i.e. C<--foo> and C<--nofoo>, works for the style in which I document them, but this is pretty ad-hoc and kludgey. More nitty-gritty documentation is needed. =head1 VERSION No version until I can get the bugs listed above under control. =head1 AUTHOR Bob Rogers C<E<lt>[EMAIL PROTECTED]<gt>>, with portions liberated from C<Pod::Usage> by Brad Appleton C<E<lt>[EMAIL PROTECTED]<gt>>. =cut use strict; use Getopt::Long; use Pod::Usage qw(); # use Data::Dumper; # define instance accessors. sub BEGIN { no strict 'refs'; for my $method (qw(option_definers additional_options man_p help_p usage_p)) { my $field = '_' . $method; *$method = sub { my $self = shift; @_ ? ($self->{$field} = shift, $self) : $self->{$field}; } } } sub new { my $class = shift; $class = ref($class) if ref($class); my $self = bless {} => $class; while (@_) { my $method = shift; my $argument = shift; $self->$method($argument) if $self->can($method); } # perform defaulting. $self->additional_options({}) unless $self->additional_options; $self->man_p(1) unless defined($self->man_p); $self->usage_p(1) unless defined($self->usage_p); $self->help_p(1) unless defined($self->help_p); $self; } ### Option generation and parsing. sub command_line_options { my $self = shift; my $cl_options = $self->{_command_line_options}; return $cl_options if $cl_options; $cl_options = { %{$self->additional_options} }; $self->{_command_line_options} = $cl_options; $cl_options->{options} = sub { $self->document_options; }; my $option_definers = $self->option_definers; if ($option_definers && @$option_definers) { for my $definer (@$option_definers) { my @options = $definer->command_line_options; while (my ($key, $value) = splice(@options, 0, 2)) { $cl_options->{$key} = $value unless $cl_options->{$key}; } } } $cl_options->{man} = sub { $self->pod2usage(-exitstatus => 0, -verbose => 2); } if $self->man_p && ! $cl_options->{man}; $cl_options->{help} = sub { $self->pod2usage(1); } if $self->help_p && ! $cl_options->{help}; $cl_options->{usage} = sub { $self->pod2usage(2); } if $self->usage_p && ! $cl_options->{usage}; $cl_options; } sub parse_options { my $self = shift; GetOptions(%{$self->command_line_options()}) or $self->pod2usage(2); # $self->document_options; } ### Class precedence table. # This is used to decide which class should be responsible for supplying the # documenting for a given option. An option defined by something earlier in the # list is assumed to override something later in the list, so the same thing # should happen for option documentation. (At least that is true for methods by # the normal rules of inheritance; some classes may have command_line_options # methods that bend this rule for options. Such classes should also have option # documentation based on that for the correct class's version of the option.) # [bug: this doesn't handle delegated objects. -- rgr, 5-Sep-04.] sub _add_to_class_table { my ($self, $table, $class_visited_p, $class) = @_; if (! $class_visited_p->{$class}) { push(@$table, $class); $class_visited_p->{$class} = 1; # now do inheritance for this class. my $class_isa; { no strict 'refs'; my $glob = *{$class."::"}->{ISA}; if ($glob && defined(*$glob{ARRAY})) { warn("[got '$glob' for $class, ", defined(*$glob{ARRAY}), ".]\n") if 0; $class_isa = *$glob{ARRAY}; } } if ($class_isa) { for my $isa (@$class_isa) { # warn "[$class isa $isa]\n"; $self->_add_to_class_table($table, $class_visited_p, $isa); } } } } sub class_precedence_table { my $self = shift; $self->{_class_precedence_table} = shift, return($self) if @_; my $table = $self->{_class_precedence_table}; return $table if $table; $self->{_class_precedence_table} = $table = ['main']; my $class_visited_p = {}; my $option_definers = $self->option_definers; for my $definer (@$option_definers, $self) { $self->_add_to_class_table($table, $class_visited_p, ref($definer) || $definer); } $table; } ### Getting option documentation. sub find_option_documentation { my ($self, $option_name) = @_; for my $class (@{$self->class_precedence_table}) { my $doc = $self->{_class_to_index}->{$class}->{$option_name}; return $doc if $doc; } ''; } sub _index_this_file { my ($self, $class, $pod) = @_; local *IN; open(IN, $pod) or die; my $index = $self->{_class_to_index}->{$class} = {}; my $option_name; while (defined(my $line = <IN>)) { if ($line =~ /^=item B<--?([^\s<>=]+)/) { # starting a new option description. if this is just a negated # version of some other option, then we must lump it with its # positive sense, since only the positive sense will appear in the # GetOptions list. [bug: we should really check that the option is # boolean, and doesn't actually appear in the list as 'no-foo'. -- # rgr, 5-Sep-04.] $option_name = $1; $option_name =~ s/^no-?//; $index->{$option_name} .= $line; } elsif ($line =~ /^=/) { # ending an option description (if one was open). $option_name = ''; } elsif ($option_name) { # next line in the current option description. $index->{$option_name} .= $line; } } close(IN); $index; } sub _index_this_class { my ($self, $class) = @_; return if $self->{_class_to_index}->{$class}; my $pod_file; if ($class eq 'main') { # [bug: should search $PATH if not found.] $pod_file = $0; } else { my $file = "$class.pm"; $file =~ s@::@/@g; # die join("\n", %INC, ''); my $source = $INC{$file}; if ($source) { my $pod = $source; $pod =~ s/\.pm$/\.pod/; $pod_file = (-r $pod ? $pod : $source); } } if (! $pod_file || ! -r $pod_file) { warn "[$self: class $class => '$pod_file', which doesn't exist.]\n"; } else { $self->_index_this_file($class, $pod_file); } } sub index_module_options { # this attempts to parse pod out of *everything*, but the result is cached, # so it's cheap to call a second time. my $self = shift; # die Dumper($self->class_precedence_table); for my $class (@{$self->class_precedence_table}) { $self->_index_this_class($class); } } sub document_options { my ($self, $fh) = @_; $fh ||= *STDOUT; my $options = $self->command_line_options; $self->index_module_options; for my $opt (sort(keys(%$options))) { my $key = $opt; $key =~ s/=[si]$|[!+]$//; my $doc = $self->find_option_documentation($key); if ($doc) { print $fh $doc; } else { # this tells the user that the option at least exists, which may be # helpful. (it should also tell the programmer that s/he didn't # finish the job). print $fh "=item B<--$key>\n\n[undocumented option]\n\n"; } } } sub pod2usage { # This duplicates much of Pod::Usage::pod2usage, since we have to do most of # the option parsing/defaulting in order to get at the -input file. my $self = shift; local $_ = shift; my %opts; ## Collect arguments if (@_ > 0) { ## Too many arguments - assume that this is a hash and ## the user forgot to pass a reference to it. %opts = ($_, @_); } elsif (ref $_) { ## User passed a ref to a hash %opts = %{$_} if (ref($_) eq 'HASH'); } elsif (/^[-+]?\d+$/) { ## User passed in the exit value to use $opts{"-exitval"} = $_; } elsif ($_) { ## User passed in a message to print before issuing usage. $opts{"-message"} = $_; } ## Default the input file $opts{"-input"} = $0 unless (defined $opts{"-input"}); ## Look up input file in path if it doesnt exist. unless ((ref $opts{"-input"}) || (-e $opts{"-input"})) { my ($dirname, $basename) = ('', $opts{"-input"}); my $pathsep = ($^O =~ /^(?:dos|os2|MSWin32)$/) ? ";" : (($^O eq 'MacOS' || $^O eq 'VMS') ? ',' : ":"); my $pathspec = $opts{"-pathlist"} || $ENV{PATH} || $ENV{PERL5LIB}; my @paths = (ref $pathspec) ? @$pathspec : split($pathsep, $pathspec); for $dirname (@paths) { $_ = File::Spec->catfile($dirname, $basename) if length; last if (-e $_) && ($opts{"-input"} = $_); } } # Transform script POD in order to insert option documentation. my $original_input = $opts{'-input'}; my $transformed_input = "CLOD$$.pod"; $transformed_input = ($ENV{TMPDIR} || $ENV{TMP}).'/'.$transformed_input if $ENV{TMPDIR} || $ENV{TMP}; die "symlink attack?" if -x $transformed_input; open(IN, $original_input) || die; $self->{_transformed_input_file} = $transformed_input; open(OUT, ">$transformed_input") || die; my $in_pod_p = 0; while (defined(my $line = <IN>)) { if ($line =~ /^=cut/) { $in_pod_p = 0; } elsif ($line =~ /^=/) { $in_pod_p = 1; } if ($line =~ /^=enumerate_options/) { $self->document_options(*OUT); } elsif ($in_pod_p) { print OUT $line; } } close(IN); close(OUT); # perform actual POD formatting. $opts{'-input'} = $transformed_input; Pod::Usage::pod2usage(\%opts); unlink($transformed_input); undef($self->{_transformed_input_file}); } sub DESTROY { # this allows us to clean up after our pod2usage without having to regain # control from Pod::Usage::pod2usage, which normally exits when done. my $self = shift; unlink($self->{_transformed_input_file}) if $self->{_transformed_input_file}; } 1; __END__ =head1 Option documentation These are the options that C<Pod::CLOD> understands natively; they are included in the option list unless specifically suppressed. =over 4 =item B<--help> Prints the L<"SYNOPSIS"> and L<"OPTIONS"> sections of this documentation. =item B<--man> Prints the full documentation in the Unix `manpage' style. =item B<--options> Prints documentation for all options in perl POD format. This is mostly useful for debugging the C<Pod::CLOD> module =item B<--usage> Prints just the L<"SYNOPSIS"> section of this documentation. =cut
_______________________________________________ Boston-pm mailing list [EMAIL PROTECTED] http://mail.pm.org/mailman/listinfo/boston-pm