]> git.madduck.net Git - code/myrepos.git/blob - mr

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

mr register: default to registering the current directory
[code/myrepos.git] / mr
1 #!/usr/bin/perl
2
3 =head1 NAME
4
5 mr - a Multiple Repository management tool
6
7 =head1 SYNOPSIS
8
9 B<mr> [options] checkout
10
11 B<mr> [options] update
12
13 B<mr> [options] status
14
15 B<mr> [options] commit [-m "message"]
16
17 B<mr> [options] diff
18
19 B<mr> [options] log
20
21 B<mr> [options] register [repository]
22
23 B<mr> [options] config section [parameter=[value] ...]
24
25 B<mr> [options] action [params ...]
26
27 =head1 DESCRIPTION
28
29 B<mr> is a Multiple Repository management tool. It allows you to register a
30 set of repositories in a .mrconfig file, and then checkout, update, or
31 perform other actions on the repositories as if they were one big
32 respository.
33
34 Any mix of revision control systems can be used with B<mr>, and you can
35 define arbitrary actions for commands like "update", "checkout", or "commit".
36
37 B<mr> cds into and operates on all registered repsitories at or below your
38 working directory. Or, if you are in a subdirectory of a repository that
39 contains no other registered repositories, it will stay in that directory,
40 and work on only that repository,
41
42 The predefined commands should be fairly familiar to users of any revision
43 control system:
44
45 =over 4
46
47 =item checkout (or co)
48
49 Checks out any repositories that are not already checked out.
50
51 =item update
52
53 Updates each repository from its configured remote repository.
54
55 If a repository isn't checked out yet, it will first check it out.
56
57 =item status
58
59 Displays a status report for each repository, showing what
60 uncommitted changes are present in the repository.
61
62 =item commit (or ci)
63
64 Commits changes to each repository. (By default, changes are pushed to the
65 remote repository too, when using distributed systems like git.)
66
67 The optional -m parameter allows specifying a commit message.
68
69 =item diff
70
71 Show a diff of uncommitted changes.
72
73 =item log
74
75 Show the commit log.
76
77 =item list (or ls)
78
79 List the repositories that mr will act on.
80
81 =item register
82
83 Register an existing repository in the mrconfig file. By default, the
84 epository in the current directory is registered, or you can specify a
85 directory to register.
86
87 =item config
88
89 Modifies the mrconfig file. The next parameter is the name of the section
90 to add or modify, and it is followed by one or more instances of
91 "parameter=value". Use "parameter=" to remove a parameter. 
92
93 For example, to add (or edit) a repository in src/foo:
94
95   mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
96
97 =item help
98
99 Displays this help.
100
101 =back
102
103 Actions can be abbreviated to any unambiguous subsctring, so
104 "mr st" is equivilant to "mr status", and "mr up" is equivilant to "mr
105 update"
106
107 Additional parameters can be passed to most commands, and are passed on
108 unchanged to the underlying revision control system. This is mostly useful
109 if the repositories mr will act on all use the same revision control
110 system.
111
112 =head1 OPTIONS
113
114 =over 4
115
116 =item -d directory
117
118 Specifies the topmost directory that B<mr> should work in. The default is
119 the current working directory.
120
121 =item -c mrconfig
122
123 Use the specified mrconfig file, instead of looking for one in your home
124 directory.
125
126 =item -v
127
128 Be verbose.
129
130 =back
131
132 =head1 FILES
133
134 B<mr> is configured by .mrconfig files. It starts by reading the .mrconfig
135 file in your home directory, and this can in turn chain load .mrconfig files
136 from repositories.
137
138 Here is an example .mrconfig file:
139
140   [src]
141   checkout = svn co svn://svn.example.com/src/trunk src
142   chain = true
143
144   [src/linux-2.6]
145   checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
146
147 The .mrconfig file uses a variant of the INI file format. Lines starting with
148 "#" are comments. Lines ending with "\" are continued on to the next line.
149
150 The "DEFAULT" section allows setting default values for the sections that
151 come after it.
152
153 The "ALIAS" section allows adding aliases for actions. Each parameter
154 is an alias, and its value is the action to use.
155
156 All other sections add repositories. The section header specifies the
157 directory where the repository is located. This is relative to the directory
158 that contains the mrconfig file, but you can also choose to use absolute
159 paths.
160
161 Within a section, each parameter defines a shell command to run to handle a
162 given action. mr contains default handlers for the "update", "status", and
163 "commit" actions, so normally you only need to specify what to do for
164 "checkout".
165
166 Note that these shell commands are run in a "set -e" shell
167 environment, where any additional parameters you pass are available in
168 "$@". The "checkout" command is run in the parent of the repository
169 directory, since the repository isn't checked out yet. All other commands
170 are run inside the repository, though not necessarily at the top of it.
171 The "MR_REPO" environment variable is set to the path to the top of the
172 repository.
173
174 A few parameters have special meanings:
175
176 =over 4
177
178 =item skip
179
180 If the "skip" parameter is set and its command returns nonzero, then B<mr>
181 will skip acting on that repository.
182
183 =item chain
184
185 If the "chain" parameter is set and its command returns nonzero, then B<mr>
186 will try to load a .mrconfig file from the root of the repository. (You
187 should avoid chaining from repositories with untrusted committers.)
188
189 =item deleted
190
191 If the "deleted" parameter is set and its command returns nonzero, then
192 B<mr> will treat the repository as deleted. It won't ever actually delete
193 the repository, but it will warn if it sees the repsoitory's directory.
194 This is useful when one mrconfig file is shared amoung multiple machines,
195 to keep track of and remember to delete old repositories.
196
197 =item lib
198
199 The "lib" parameter can specify some shell code that will be run before each
200 command, this can be a useful way to define shell functions for other commands
201 to use.
202
203 =back
204
205 =head1 AUTHOR
206
207 Copyright 2007 Joey Hess <joey@kitenet.net>
208
209 Licensed under the GNU GPL version 2 or higher.
210
211 http://kitenet.net/~joey/code/mr/
212
213 =cut
214
215 use warnings;
216 use strict;
217 use Getopt::Long;
218 use Cwd qw(getcwd abs_path);
219
220 my $directory=getcwd();
221 my $config="$ENV{HOME}/.mrconfig";
222 my $verbose=0;
223 my %config;
224 my %knownactions;
225 my %alias;
226
227 Getopt::Long::Configure("no_permute");
228 my $result=GetOptions(
229         "d|directory=s" => sub { $directory=abs_path($_[1]) },
230         "c|config=s" => \$config,
231         "verbose" => \$verbose,
232 );
233 if (! $result || @ARGV < 1) {
234         die("Usage: mr [-d directory] action [params ...]\n".
235             "(Use mr help for man page.)\n");
236
237 }
238
239 loadconfig(\*DATA);
240 loadconfig($config);
241 #use Data::Dumper;
242 #print Dumper(\%config);
243
244 eval {
245         use FindBin qw($Bin $Script);
246         $ENV{MR_PATH}=$Bin."/".$Script;
247 };
248
249 # alias expansion and command stemming
250 my $action=shift @ARGV;
251 if (exists $alias{$action}) {
252         $action=$alias{$action};
253 }
254 if (! exists $knownactions{$action}) {
255         my @matches = grep { /^\Q$action\E/ }
256                 keys %knownactions, keys %alias;
257         if (@matches == 1) {
258                 $action=$matches[0];
259         }
260         elsif (@matches == 0) {
261                 die "mr: unknown action \"$action\" (known actions: ".
262                         join(", ", sort keys %knownactions).")\n";
263         }
264         else {
265                 die "mr: ambiguous action \"$action\" (matches: ".
266                         join(", ", @matches).")\n";
267         }
268 }
269
270 if ($action eq 'help') {
271         exec($config{''}{DEFAULT}{$action}) || die "exec: $!";
272 }
273 elsif ($action eq 'config') {
274         if (@ARGV < 2) {
275                 die "mr config: not enough parameters\n";
276         }
277         my $section=shift;
278         if ($section=~/^\//) {
279                 # try to convert to a path relative to $config's dir
280                 my ($dir)=$config=~/^(.*\/)[^\/]+$/;
281                 if ($section=~/^\Q$dir\E(.*)/) {
282                         $section=$1;
283                 }
284         }
285         my %fields;
286         foreach (@ARGV) {
287                 if (/^([^=]+)=(.*)$/) {
288                         $fields{$1}=$2;
289                 }
290                 else {
291                         die "mr config: expected parameter=value, not \"$_\"\n";
292                 }
293         }
294         modifyconfig($config, $section, %fields);
295         exit 0;
296 }
297 elsif ($action eq 'register') {
298         my $command="set -e; ".$config{''}{DEFAULT}{lib}."\n".
299                 "my_action(){ $config{''}{DEFAULT}{$action}\n }; my_action ".
300                 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
301         print STDERR "mr $action: running >>$command<<\n" if $verbose;
302         exec($command) || die "exec: $!";
303 }
304
305 # work out what repos to act on
306 my @repos;
307 my $nochdir=0;
308 foreach my $topdir (sort keys %config) {
309         foreach my $subdir (sort keys %{$config{$topdir}}) {
310                 next if $subdir eq 'DEFAULT';
311                 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
312                 my $d=$directory;
313                 $dir.="/" unless $dir=~/\/$/;
314                 $d.="/" unless $d=~/\/$/;
315                 next if $dir ne $directory && $dir !~ /^\Q$directory\E/;
316                 push @repos, [$dir, $topdir, $subdir];
317         }
318 }
319 if (! @repos) {
320         # fallback to find a leaf repo
321         LEAF: foreach my $topdir (reverse sort keys %config) {
322                 foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
323                         next if $subdir eq 'DEFAULT';
324                         my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
325                         my $d=$directory;
326                         $dir.="/" unless $dir=~/\/$/;
327                         $d.="/" unless $d=~/\/$/;
328                         if ($d=~/^\Q$dir\E/) {
329                                 push @repos, [$dir, $topdir, $subdir];
330                                 last LEAF;
331                         }
332                 }
333         }
334         $nochdir=1;
335 }
336
337 my (@failed, @successful, @skipped);
338 foreach my $repo (@repos) {
339         action($action, @$repo);
340 }
341
342 sub action {
343         my ($action, $dir, $topdir, $subdir) = @_;
344         
345         my $lib= exists $config{$topdir}{$subdir}{lib} ?
346                         $config{$topdir}{$subdir}{lib}."\n" : "";
347
348         if (exists $config{$topdir}{$subdir}{deleted}) {
349                 if (! -d $dir) {
350                         return;
351                 }
352                 else {
353                         my $test="set -e;".$lib.$config{$topdir}{$subdir}{deleted};
354                         print "mr $action: running deleted test >>$test<<\n" if $verbose;
355                         my $ret=system($test);
356                         if ($ret >> 8 == 0) {
357                                 print STDERR "mr error: $dir should be deleted yet still exists\n\n";
358                                 push @failed, $dir;
359                                 return;
360                         }
361                 }
362         }
363
364         if ($action eq 'checkout') {
365                 if (-d $dir) {
366                         print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
367                         push @skipped, $dir;
368                         return;
369                 }
370                 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
371         }
372         elsif ($action eq 'update') {
373                 if (! -d $dir) {
374                         return action("checkout", $dir, $topdir, $subdir);
375                 }
376         }
377         
378         $ENV{MR_REPO}=$dir;
379
380         if (exists $config{$topdir}{$subdir}{skip}) {
381                 my $test="set -e;".$lib.$config{$topdir}{$subdir}{skip};
382                 print "mr $action: running skip test >>$test<<\n" if $verbose;
383                 my $ret=system($test);
384                 if ($ret >> 8 == 0) {
385                         print "mr $action: $dir skipped per config file\n" if $verbose;
386                         push @skipped, $dir;
387                         return;
388                 }
389         }
390         
391         if (! $nochdir && ! chdir($dir)) {
392                 print STDERR "mr $action: failed to chdir to $dir: $!\n";
393                 push @failed, $dir;
394         }
395         elsif (! exists $config{$topdir}{$subdir}{$action}) {
396                 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
397                 push @skipped, $dir;
398         }
399         else {
400                 if (! $nochdir) {
401                         print "mr $action: $topdir$subdir\n";
402                 }
403                 else {
404                         print "mr $action: $topdir$subdir (in subdir $directory)\n";
405                 }
406                 my $command="set -e; ".$lib.
407                         "my_action(){ $config{$topdir}{$subdir}{$action}\n }; my_action ".
408                         join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
409                 print STDERR "mr $action: running >>$command<<\n" if $verbose;
410                 my $ret=system($command);
411                 if ($ret != 0) {
412                         print STDERR "mr $action: failed ($ret)\n" if $verbose;
413                         push @failed, $dir;
414                         if ($ret >> 8 != 0) {
415                                 print STDERR "mr $action: command failed\n";
416                         }
417                         elsif ($ret != 0) {
418                                 print STDERR "mr $action: command died ($ret)\n";
419                         }
420                 }
421                 else {
422                         push @successful, $dir;
423                 }
424
425                 print "\n";
426         }
427 }
428
429 sub showstat {
430         my $count=shift;
431         my $singular=shift;
432         my $plural=shift;
433         if ($count) {
434                 return "$count ".($count > 1 ? $plural : $singular);
435         }
436         return;
437 }
438 if (! @successful && ! @failed && ! @skipped) {
439         die "mr $action: no repositories found to work on\n";
440 }
441 print "mr $action: finished (".join("; ",
442         showstat($#successful+1, "successful", "successful"),
443         showstat($#failed+1, "failed", "failed"),
444         showstat($#skipped+1, "skipped", "skipped"),
445 ).")\n";
446 if (@failed) {
447         exit 1;
448 }
449 elsif (! @successful && @skipped) {
450         exit 1;
451 }
452 exit 0;
453
454 my %loaded;
455 sub loadconfig {
456         my $f=shift;
457
458         my @toload;
459
460         my $in;
461         my $dir;
462         if (ref $f eq 'GLOB') {
463                 $in=$f; 
464                 $dir="";
465         }
466         else {
467                 if (! -e $f) {
468                         return;
469                 }
470
471                 my $absf=abs_path($f);
472                 if ($loaded{$absf}) {
473                         return;
474                 }
475                 $loaded{$absf}=1;
476
477                 print "mr: loading config $f\n" if $verbose;
478                 open($in, "<", $f) || die "mr: open $f: $!\n";
479                 ($dir)=$f=~/^(.*\/)[^\/]+$/;
480                 if (! defined $dir) {
481                         $dir=".";
482                 }
483                 $dir=abs_path($dir)."/";
484
485                 # copy in defaults from first parent
486                 my $parent=$dir;
487                 while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
488                         if (exists $config{$parent} &&
489                             exists $config{$parent}{DEFAULT}) {
490                                 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
491                                 last;
492                         }
493                 }
494         }
495
496         my $section;
497         while (<$in>) {
498                 chomp;
499                 next if /^\s*\#/ || /^\s*$/;
500                 if (/^\s*\[([^\]]*)\]\s*$/) {
501                         $section=$1;
502                 }
503                 elsif (/^\s*(\w+)\s*=\s*(.*)/) {
504                         my $parameter=$1;
505                         my $value=$2;
506
507                         # continuation line
508                         while ($value=~/(.*)\\$/s) {
509                                 $value=$1."\n".<$in>;
510                                 chomp $value;
511                         }
512
513                         if (! defined $section) {
514                                 die "$f line $.: parameter ($parameter) not in section\n";
515                         }
516                         if ($section ne 'ALIAS' &&
517                             ! exists $config{$dir}{$section} &&
518                             exists $config{$dir}{DEFAULT}) {
519                                 # copy in defaults
520                                 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
521                         }
522                         if ($section eq 'ALIAS') {
523                                 $alias{$parameter}=$value;
524                         }
525                         elsif ($parameter eq 'lib') {
526                                 $config{$dir}{$section}{lib}.=$value."\n";
527                         }
528                         else {
529                                 $config{$dir}{$section}{$parameter}=$value;
530                                 $knownactions{$parameter}=1;
531                                 if ($parameter eq 'chain' &&
532                                     length $dir && $section ne "DEFAULT" &&
533                                     -e $dir.$section."/.mrconfig" &&
534                                     system($value) >> 8 == 0) {
535                                         push @toload, $dir.$section."/.mrconfig";
536                                 }
537                         }
538                 }
539                 else {
540                         die "$f line $.: parse error\n";
541                 }
542         }
543         close $in;
544
545         foreach (@toload) {
546                 loadconfig($_);
547         }
548 }
549
550 sub modifyconfig {
551         my $f=shift;
552         # the section to modify or add
553         my $targetsection=shift;
554         # fields to change in the section
555         # To remove a field, set its value to "".
556         my %changefields=@_;
557
558         my @lines;
559         my @out;
560
561         if (-e $f) {
562                 open(my $in, "<", $f) || die "mr: open $f: $!\n";
563                 @lines=<$in>;
564                 close $in;
565         }
566
567         my $addfields=sub {
568                 my @blanks;
569                 while ($out[$#out] =~ /^\s*$/) {
570                         unshift @blanks, pop @out;
571                 }
572                 foreach my $field (sort keys %changefields) {
573                         if (length $changefields{$field}) {
574                                 push @out, "$field = $changefields{$field}\n";
575                         }
576                 }
577                 push @out, @blanks;
578         };
579
580         my $section;
581         while (@lines) {
582                 $_=shift(@lines);
583
584                 if (/^\s*\#/ || /^\s*$/) {
585                         push @out, $_;
586                 }
587                 elsif (/^\s*\[([^\]]*)\]\s*$/) {
588                         if (defined $section && 
589                             $section eq $targetsection) {
590                                 $addfields->();
591                         }
592
593                         $section=$1;
594
595                         push @out, $_;
596                 }
597                 elsif (/^\s*(\w+)\s*=\s(.*)/) {
598                         my $parameter=$1;
599                         my $value=$2;
600
601                         # continuation line
602                         while ($value=~/(.*\\)$/s) {
603                                 $value=$1."\n".shift(@lines);
604                                 chomp $value;
605                         }
606
607                         if ($section eq $targetsection) {
608                                 if (exists $changefields{$parameter}) {
609                                         if (length $changefields{$parameter}) {
610                                                 $value=$changefields{$parameter};
611                                         }
612                                         delete $changefields{$parameter};
613                                 }
614                         }
615
616                         push @out, "$parameter = $value\n";
617                 }
618         }
619
620         if (defined $section && 
621             $section eq $targetsection) {
622                         $addfields->();
623         }
624         elsif (%changefields) {
625                 push @out, "\n[$targetsection]\n";
626                 foreach my $field (sort keys %changefields) {
627                         if (length $changefields{$field}) {
628                                 push @out, "$field = $changefields{$field}\n";
629                         }
630                 }
631         }
632
633         open(my $out, ">", $f) || die "mr: write $f: $!\n";
634         print $out @out;
635         close $out;     
636 }
637
638 # Finally, some useful actions that mr knows about by default.
639 # These can be overridden in ~/.mrconfig.
640 __DATA__
641 [ALIAS]
642         co = checkout
643         ci = commit
644         ls = list
645
646 [DEFAULT]
647 lib =                                                   \
648         error() {                                       \
649                 echo "mr: $@" >&2                       \
650                 exit 1                                  \
651         }
652
653 update =                                                \
654         if [ -d "$MR_REPO"/.svn ]; then                 \
655                 svn update "$@"                         \
656         elif [ -d "$MR_REPO"/.git ]; then               \
657                 git pull origin master "$@"             \
658         elif [ -d "$MR_REPO"/.bzr ]; then               \
659                 bzr merge "$@"                          \
660         elif [ -d "$MR_REPO"/CVS ]; then                \
661                 cvs update "$@"                         \
662         else                                            \
663                 error "unknown repo type"               \
664         fi
665 status =                                                \
666         if [ -d "$MR_REPO"/.svn ]; then                 \
667                 svn status "$@"                         \
668         elif [ -d "$MR_REPO"/.git ]; then               \
669                 git status "$@" || true                 \
670         elif [ -d "$MR_REPO"/.bzr ]; then               \
671                 bzr status "$@"                         \
672         elif [ -d "$MR_REPO"/CVS ]; then                \
673                 cvs status "$@"                         \
674         else                                            \
675                 error "unknown repo type"               \
676         fi
677 commit =                                                \
678         if [ -d "$MR_REPO"/.svn ]; then                 \
679                 svn commit "$@"                         \
680         elif [ -d "$MR_REPO"/.git ]; then               \
681                 git commit -a "$@" && git push --all    \
682         elif [ -d "$MR_REPO"/.bzr ]; then               \
683                 bzr commit "$@" && bzr push             \
684         elif [ -d "$MR_REPO"/CVS ]; then                \
685                 cvs commit "$@"                         \
686         else                                            \
687                 error "unknown repo type"               \
688         fi
689 diff =                                                  \
690         if [ -d "$MR_REPO"/.svn ]; then                 \
691                 svn diff "$@"                           \
692         elif [ -d "$MR_REPO"/.git ]; then               \
693                 git diff "$@"                           \
694         elif [ -d "$MR_REPO"/.bzr ]; then               \
695                 bzr diff "$@"                           \
696         elif [ -d "$MR_REPO"/CVS ]; then                \
697                 cvs diff "$@"                           \
698         else                                            \
699                 error "unknown repo type"               \
700         fi
701 log =                                                   \
702         if [ -d "$MR_REPO"/.svn ]; then                 \
703                 svn log"$@"                             \
704         elif [ -d "$MR_REPO"/.git ]; then               \
705                 git log "$@"                            \
706         elif [ -d "$MR_REPO"/.bzr ]; then               \
707                 bzr log "$@"                            \
708         elif [ -d "$MR_REPO"/CVS ]; then                \
709                 cvs log "$@"                            \
710         else                                            \
711                 error "unknown repo type"               \
712         fi
713 register =                                                              \
714         if [ -n "$1" ]; then                                            \
715                 cd "$1"                                                 \
716         fi                                                              \
717         basedir="$(basename $(pwd))"                                    \
718         if [ -d .svn ]; then                                            \
719                 url=$(LANG=C svn info . |                               \
720                       grep -i ^URL: | cut -d ' ' -f 2)                  \
721                 if [ -z "$url" ]; then                                  \
722                         error "cannot determine svn url"                \
723                 fi                                                      \
724                 echo "Registering svn url: $url"                        \
725                 mr config "$(pwd)" checkout="svn co $url $basedir"      \
726         elif [ -d .git ]; then                                          \
727                 url=$(LANG=C git-config --get remote.origin.url)        \
728                 if [ -z "$url" ]; then                                  \
729                         error "cannot determine git url"                \
730                 fi                                                      \
731                 echo "Registering git url: $url"                        \
732                 mr config "$(pwd)" checkout="git clone $url $basedir"   \
733         elif [ -d .bzr ]; then                                          \
734                 url=$(cat .bzr/branch/parent)                           \
735                 if [ -z "$url" ]; then                                  \
736                         error "cannot determine bzr url"                \
737                 fi                                                      \
738                 echo "Registering bzr url: $url"                        \
739                 mr config "$(pwd)" checkout="bzr clone $url $basedir"   \
740         else                                                            \
741                 error "unable to register this repo type"               \
742         fi
743 list = true
744 config = 
745 help =                                                  \
746         if [ ! -e "$MR_PATH" ]; then                    \
747                 error "cannot find program path"        \
748         fi                                              \
749         (pod2man -c mr "$MR_PATH" | man -l -) ||        \
750                 error "pod2man or man failed"
751
752 ed = echo "A horse is a horse, of course, of course.."
753 T = echo "I pity the fool."