scripts/get_maintainer.pl: add interactive mode
[pandora-kernel.git] / scripts / get_maintainer.pl
1 #!/usr/bin/perl -w
2 # (c) 2007, Joe Perches <joe@perches.com>
3 #           created from checkpatch.pl
4 #
5 # Print selected MAINTAINERS information for
6 # the files modified in a patch or for a file
7 #
8 # usage: perl scripts/get_maintainer.pl [OPTIONS] <patch>
9 #        perl scripts/get_maintainer.pl [OPTIONS] -f <file>
10 #
11 # Licensed under the terms of the GNU GPL License version 2
12
13 use strict;
14
15 my $P = $0;
16 my $V = '0.25';
17
18 use Getopt::Long qw(:config no_auto_abbrev);
19
20 my $lk_path = "./";
21 my $email = 1;
22 my $email_usename = 1;
23 my $email_maintainer = 1;
24 my $email_list = 1;
25 my $email_subscriber_list = 0;
26 my $email_git_penguin_chiefs = 0;
27 my $email_git = 0;
28 my $email_git_all_signature_types = 0;
29 my $email_git_blame = 0;
30 my $email_git_fallback = 1;
31 my $email_git_min_signatures = 1;
32 my $email_git_max_maintainers = 5;
33 my $email_git_min_percent = 5;
34 my $email_git_since = "1-year-ago";
35 my $email_hg_since = "-365";
36 my $interactive = 0;
37 my $email_remove_duplicates = 1;
38 my $output_multiline = 1;
39 my $output_separator = ", ";
40 my $output_roles = 0;
41 my $output_rolestats = 0;
42 my $scm = 0;
43 my $web = 0;
44 my $subsystem = 0;
45 my $status = 0;
46 my $keywords = 1;
47 my $sections = 0;
48 my $file_emails = 0;
49 my $from_filename = 0;
50 my $pattern_depth = 0;
51 my $version = 0;
52 my $help = 0;
53
54 my $exit = 0;
55
56 my %shortlog_buffer;
57
58 my @penguin_chief = ();
59 push(@penguin_chief, "Linus Torvalds:torvalds\@linux-foundation.org");
60 #Andrew wants in on most everything - 2009/01/14
61 #push(@penguin_chief, "Andrew Morton:akpm\@linux-foundation.org");
62
63 my @penguin_chief_names = ();
64 foreach my $chief (@penguin_chief) {
65     if ($chief =~ m/^(.*):(.*)/) {
66         my $chief_name = $1;
67         my $chief_addr = $2;
68         push(@penguin_chief_names, $chief_name);
69     }
70 }
71 my $penguin_chiefs = "\(" . join("|", @penguin_chief_names) . "\)";
72
73 # Signature types of people who are either
74 #       a) responsible for the code in question, or
75 #       b) familiar enough with it to give relevant feedback
76 my @signature_tags = ();
77 push(@signature_tags, "Signed-off-by:");
78 push(@signature_tags, "Reviewed-by:");
79 push(@signature_tags, "Acked-by:");
80 my $signaturePattern = "\(" . join("|", @signature_tags) . "\)";
81
82 # rfc822 email address - preloaded methods go here.
83 my $rfc822_lwsp = "(?:(?:\\r\\n)?[ \\t])";
84 my $rfc822_char = '[\\000-\\377]';
85
86 # VCS command support: class-like functions and strings
87
88 my %VCS_cmds;
89
90 my %VCS_cmds_git = (
91     "execute_cmd" => \&git_execute_cmd,
92     "available" => '(which("git") ne "") && (-d ".git")',
93     "find_signers_cmd" => "git log --no-color --since=\$email_git_since -- \$file",
94     "find_commit_signers_cmd" => "git log --no-color -1 \$commit",
95     "find_commit_author_cmd" => "git log -1 --format=\"%an <%ae>\" \$commit",
96     "blame_range_cmd" => "git blame -l -L \$diff_start,+\$diff_length \$file",
97     "blame_file_cmd" => "git blame -l \$file",
98     "commit_pattern" => "^commit [0-9a-f]{40,40}",
99     "blame_commit_pattern" => "^([0-9a-f]+) ",
100     "shortlog_cmd" => "git log --no-color --oneline --since=\$email_git_since --author=\"\$email\" -- \$file"
101 );
102
103 my %VCS_cmds_hg = (
104     "execute_cmd" => \&hg_execute_cmd,
105     "available" => '(which("hg") ne "") && (-d ".hg")',
106     "find_signers_cmd" =>
107         "hg log --date=\$email_hg_since" .
108                 " --template='commit {node}\\n{desc}\\n' -- \$file",
109     "find_commit_signers_cmd" => "hg log --template='{desc}\\n' -r \$commit",
110     "find_commit_author_cmd" => "hg log -l 1 --template='{author}\\n' -r \$commit",
111     "blame_range_cmd" => "",            # not supported
112     "blame_file_cmd" => "hg blame -c \$file",
113     "commit_pattern" => "^commit [0-9a-f]{40,40}",
114     "blame_commit_pattern" => "^([0-9a-f]+):",
115     "shortlog_cmd" => "ht log --date=\$email_hg_since"
116 );
117
118 my $conf = which_conf(".get_maintainer.conf");
119 if (-f $conf) {
120     my @conf_args;
121     open(my $conffile, '<', "$conf")
122         or warn "$P: Can't find a readable .get_maintainer.conf file $!\n";
123
124     while (<$conffile>) {
125         my $line = $_;
126
127         $line =~ s/\s*\n?$//g;
128         $line =~ s/^\s*//g;
129         $line =~ s/\s+/ /g;
130
131         next if ($line =~ m/^\s*#/);
132         next if ($line =~ m/^\s*$/);
133
134         my @words = split(" ", $line);
135         foreach my $word (@words) {
136             last if ($word =~ m/^#/);
137             push (@conf_args, $word);
138         }
139     }
140     close($conffile);
141     unshift(@ARGV, @conf_args) if @conf_args;
142 }
143
144 if (!GetOptions(
145                 'email!' => \$email,
146                 'git!' => \$email_git,
147                 'git-all-signature-types!' => \$email_git_all_signature_types,
148                 'git-blame!' => \$email_git_blame,
149                 'git-fallback!' => \$email_git_fallback,
150                 'git-chief-penguins!' => \$email_git_penguin_chiefs,
151                 'git-min-signatures=i' => \$email_git_min_signatures,
152                 'git-max-maintainers=i' => \$email_git_max_maintainers,
153                 'git-min-percent=i' => \$email_git_min_percent,
154                 'git-since=s' => \$email_git_since,
155                 'hg-since=s' => \$email_hg_since,
156                 'i|interactive!' => \$interactive,
157                 'remove-duplicates!' => \$email_remove_duplicates,
158                 'm!' => \$email_maintainer,
159                 'n!' => \$email_usename,
160                 'l!' => \$email_list,
161                 's!' => \$email_subscriber_list,
162                 'multiline!' => \$output_multiline,
163                 'roles!' => \$output_roles,
164                 'rolestats!' => \$output_rolestats,
165                 'separator=s' => \$output_separator,
166                 'subsystem!' => \$subsystem,
167                 'status!' => \$status,
168                 'scm!' => \$scm,
169                 'web!' => \$web,
170                 'pattern-depth=i' => \$pattern_depth,
171                 'k|keywords!' => \$keywords,
172                 'sections!' => \$sections,
173                 'fe|file-emails!' => \$file_emails,
174                 'f|file' => \$from_filename,
175                 'v|version' => \$version,
176                 'h|help|usage' => \$help,
177                 )) {
178     die "$P: invalid argument - use --help if necessary\n";
179 }
180
181 if ($help != 0) {
182     usage();
183     exit 0;
184 }
185
186 if ($version != 0) {
187     print("${P} ${V}\n");
188     exit 0;
189 }
190
191 if (-t STDIN && !@ARGV) {
192     # We're talking to a terminal, but have no command line arguments.
193     die "$P: missing patchfile or -f file - use --help if necessary\n";
194 }
195
196 if ($output_separator ne ", ") {
197     $output_multiline = 0;
198 }
199
200 if ($output_rolestats) {
201     $output_roles = 1;
202 }
203
204 if ($sections) {
205     $email = 0;
206     $email_list = 0;
207     $scm = 0;
208     $status = 0;
209     $subsystem = 0;
210     $web = 0;
211     $keywords = 0;
212 } else {
213     my $selections = $email + $scm + $status + $subsystem + $web;
214     if ($selections == 0) {
215         die "$P:  Missing required option: email, scm, status, subsystem or web\n";
216     }
217 }
218
219 if ($email &&
220     ($email_maintainer + $email_list + $email_subscriber_list +
221      $email_git + $email_git_penguin_chiefs + $email_git_blame) == 0) {
222     die "$P: Please select at least 1 email option\n";
223 }
224
225 if (!top_of_kernel_tree($lk_path)) {
226     die "$P: The current directory does not appear to be "
227         . "a linux kernel source tree.\n";
228 }
229
230 if ($email_git_all_signature_types) {
231     $signaturePattern = "(.+?)[Bb][Yy]:";
232 }
233
234
235
236 ## Read MAINTAINERS for type/value pairs
237
238 my @typevalue = ();
239 my %keyword_hash;
240
241 open (my $maint, '<', "${lk_path}MAINTAINERS")
242     or die "$P: Can't open MAINTAINERS: $!\n";
243 while (<$maint>) {
244     my $line = $_;
245
246     if ($line =~ m/^(\C):\s*(.*)/) {
247         my $type = $1;
248         my $value = $2;
249
250         ##Filename pattern matching
251         if ($type eq "F" || $type eq "X") {
252             $value =~ s@\.@\\\.@g;       ##Convert . to \.
253             $value =~ s/\*/\.\*/g;       ##Convert * to .*
254             $value =~ s/\?/\./g;         ##Convert ? to .
255             ##if pattern is a directory and it lacks a trailing slash, add one
256             if ((-d $value)) {
257                 $value =~ s@([^/])$@$1/@;
258             }
259         } elsif ($type eq "K") {
260             $keyword_hash{@typevalue} = $value;
261         }
262         push(@typevalue, "$type:$value");
263     } elsif (!/^(\s)*$/) {
264         $line =~ s/\n$//g;
265         push(@typevalue, $line);
266     }
267 }
268 close($maint);
269
270 my %mailmap;
271
272 if ($email_remove_duplicates) {
273     open(my $mailmap, '<', "${lk_path}.mailmap")
274         or warn "$P: Can't open .mailmap: $!\n";
275     while (<$mailmap>) {
276         my $line = $_;
277
278         next if ($line =~ m/^\s*#/);
279         next if ($line =~ m/^\s*$/);
280
281         my ($name, $address) = parse_email($line);
282         $line = format_email($name, $address, $email_usename);
283
284         next if ($line =~ m/^\s*$/);
285
286         if (exists($mailmap{$name})) {
287             my $obj = $mailmap{$name};
288             push(@$obj, $address);
289         } else {
290             my @arr = ($address);
291             $mailmap{$name} = \@arr;
292         }
293     }
294     close($mailmap);
295 }
296
297 ## use the filenames on the command line or find the filenames in the patchfiles
298
299 my @files = ();
300 my @range = ();
301 my @keyword_tvi = ();
302 my @file_emails = ();
303
304 if (!@ARGV) {
305     push(@ARGV, "&STDIN");
306 }
307
308 foreach my $file (@ARGV) {
309     if ($file ne "&STDIN") {
310         ##if $file is a directory and it lacks a trailing slash, add one
311         if ((-d $file)) {
312             $file =~ s@([^/])$@$1/@;
313         } elsif (!(-f $file)) {
314             die "$P: file '${file}' not found\n";
315         }
316     }
317     if ($from_filename) {
318         push(@files, $file);
319         if ($file ne "MAINTAINERS" && -f $file && ($keywords || $file_emails)) {
320             open(my $f, '<', $file)
321                 or die "$P: Can't open $file: $!\n";
322             my $text = do { local($/) ; <$f> };
323             close($f);
324             if ($keywords) {
325                 foreach my $line (keys %keyword_hash) {
326                     if ($text =~ m/$keyword_hash{$line}/x) {
327                         push(@keyword_tvi, $line);
328                     }
329                 }
330             }
331             if ($file_emails) {
332                 my @poss_addr = $text =~ m$[A-Za-zÀ-ÿ\"\' \,\.\+-]*\s*[\,]*\s*[\(\<\{]{0,1}[A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+\.[A-Za-z0-9]+[\)\>\}]{0,1}$g;
333                 push(@file_emails, clean_file_emails(@poss_addr));
334             }
335         }
336     } else {
337         my $file_cnt = @files;
338         my $lastfile;
339
340         open(my $patch, "< $file")
341             or die "$P: Can't open $file: $!\n";
342         while (<$patch>) {
343             my $patch_line = $_;
344             if (m/^\+\+\+\s+(\S+)/) {
345                 my $filename = $1;
346                 $filename =~ s@^[^/]*/@@;
347                 $filename =~ s@\n@@;
348                 $lastfile = $filename;
349                 push(@files, $filename);
350             } elsif (m/^\@\@ -(\d+),(\d+)/) {
351                 if ($email_git_blame) {
352                     push(@range, "$lastfile:$1:$2");
353                 }
354             } elsif ($keywords) {
355                 foreach my $line (keys %keyword_hash) {
356                     if ($patch_line =~ m/^[+-].*$keyword_hash{$line}/x) {
357                         push(@keyword_tvi, $line);
358                     }
359                 }
360             }
361         }
362         close($patch);
363
364         if ($file_cnt == @files) {
365             warn "$P: file '${file}' doesn't appear to be a patch.  "
366                 . "Add -f to options?\n";
367         }
368         @files = sort_and_uniq(@files);
369     }
370 }
371
372 @file_emails = uniq(@file_emails);
373
374 my @email_to = ();
375 my @list_to = ();
376 my @scm = ();
377 my @web = ();
378 my @subsystem = ();
379 my @status = ();
380
381 # Find responsible parties
382
383 foreach my $file (@files) {
384
385     my %hash;
386     my $exact_pattern_match = 0;
387     my $tvi = find_first_section();
388     while ($tvi < @typevalue) {
389         my $start = find_starting_index($tvi);
390         my $end = find_ending_index($tvi);
391         my $exclude = 0;
392         my $i;
393
394         #Do not match excluded file patterns
395
396         for ($i = $start; $i < $end; $i++) {
397             my $line = $typevalue[$i];
398             if ($line =~ m/^(\C):\s*(.*)/) {
399                 my $type = $1;
400                 my $value = $2;
401                 if ($type eq 'X') {
402                     if (file_match_pattern($file, $value)) {
403                         $exclude = 1;
404                         last;
405                     }
406                 }
407             }
408         }
409
410         if (!$exclude) {
411             for ($i = $start; $i < $end; $i++) {
412                 my $line = $typevalue[$i];
413                 if ($line =~ m/^(\C):\s*(.*)/) {
414                     my $type = $1;
415                     my $value = $2;
416                     if ($type eq 'F') {
417                         if (file_match_pattern($file, $value)) {
418                             my $value_pd = ($value =~ tr@/@@);
419                             my $file_pd = ($file  =~ tr@/@@);
420                             $value_pd++ if (substr($value,-1,1) ne "/");
421                             $value_pd = -1 if ($value =~ /^\.\*/);
422                             $exact_pattern_match = 1 if ($value_pd >= $file_pd);
423                             if ($pattern_depth == 0 ||
424                                 (($file_pd - $value_pd) < $pattern_depth)) {
425                                 $hash{$tvi} = $value_pd;
426                             }
427                         }
428                     }
429                 }
430             }
431         }
432
433         $tvi = $end + 1;
434     }
435
436     foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
437         add_categories($line);
438         if ($sections) {
439             my $i;
440             my $start = find_starting_index($line);
441             my $end = find_ending_index($line);
442             for ($i = $start; $i < $end; $i++) {
443                 my $line = $typevalue[$i];
444                 if ($line =~ /^[FX]:/) {                ##Restore file patterns
445                     $line =~ s/([^\\])\.([^\*])/$1\?$2/g;
446                     $line =~ s/([^\\])\.$/$1\?/g;       ##Convert . back to ?
447                     $line =~ s/\\\./\./g;               ##Convert \. to .
448                     $line =~ s/\.\*/\*/g;               ##Convert .* to *
449                 }
450                 $line =~ s/^([A-Z]):/$1:\t/g;
451                 print("$line\n");
452             }
453             print("\n");
454         }
455     }
456
457     if ($email &&
458         ($email_git || ($email_git_fallback && !$exact_pattern_match))) {
459         vcs_file_signoffs($file);
460     }
461     if ($email && $email_git_blame) {
462         vcs_file_blame($file);
463     }
464     if ($email && $interactive){
465         vcs_file_shortlogs($file);
466
467     }
468 }
469
470 if ($keywords) {
471     @keyword_tvi = sort_and_uniq(@keyword_tvi);
472     foreach my $line (@keyword_tvi) {
473         add_categories($line);
474     }
475 }
476
477 if ($email) {
478     foreach my $chief (@penguin_chief) {
479         if ($chief =~ m/^(.*):(.*)/) {
480             my $email_address;
481
482             $email_address = format_email($1, $2, $email_usename);
483             if ($email_git_penguin_chiefs) {
484                 push(@email_to, [$email_address, 'chief penguin']);
485             } else {
486                 @email_to = grep($_->[0] !~ /${email_address}/, @email_to);
487             }
488         }
489     }
490
491     foreach my $email (@file_emails) {
492         my ($name, $address) = parse_email($email);
493
494         my $tmp_email = format_email($name, $address, $email_usename);
495         push_email_address($tmp_email, '');
496         add_role($tmp_email, 'in file');
497     }
498 }
499
500
501 if ($email || $email_list) {
502     my @to = ();
503     if ($email) {
504         if ($interactive) {
505             @email_to = @{vcs_interactive_menu(\@email_to)};
506         }
507         @to = (@to, @email_to);
508     }
509     if ($email_list) {
510         @to = (@to, @list_to);
511     }
512     output(merge_email(@to));
513 }
514
515 if ($scm) {
516     @scm = uniq(@scm);
517     output(@scm);
518 }
519 if ($status) {
520     @status = uniq(@status);
521     output(@status);
522 }
523
524 if ($subsystem) {
525     @subsystem = uniq(@subsystem);
526     output(@subsystem);
527 }
528
529 if ($web) {
530     @web = uniq(@web);
531     output(@web);
532 }
533
534 exit($exit);
535
536 sub file_match_pattern {
537     my ($file, $pattern) = @_;
538     if (substr($pattern, -1) eq "/") {
539         if ($file =~ m@^$pattern@) {
540             return 1;
541         }
542     } else {
543         if ($file =~ m@^$pattern@) {
544             my $s1 = ($file =~ tr@/@@);
545             my $s2 = ($pattern =~ tr@/@@);
546             if ($s1 == $s2) {
547                 return 1;
548             }
549         }
550     }
551     return 0;
552 }
553
554 sub usage {
555     print <<EOT;
556 usage: $P [options] patchfile
557        $P [options] -f file|directory
558 version: $V
559
560 MAINTAINER field selection options:
561   --email => print email address(es) if any
562     --git => include recent git \*-by: signers
563     --git-all-signature-types => include signers regardless of signature type
564         or use only ${signaturePattern} signers (default: $email_git_all_signature_types)
565     --git-fallback => use git when no exact MAINTAINERS pattern (default: $email_git_fallback)
566     --git-chief-penguins => include ${penguin_chiefs}
567     --git-min-signatures => number of signatures required (default: $email_git_min_signatures)
568     --git-max-maintainers => maximum maintainers to add (default: $email_git_max_maintainers)
569     --git-min-percent => minimum percentage of commits required (default: $email_git_min_percent)
570     --git-blame => use git blame to find modified commits for patch or file
571     --git-since => git history to use (default: $email_git_since)
572     --hg-since => hg history to use (default: $email_hg_since)
573     --interactive => display a menu (mostly useful if used with the --git option)
574     --m => include maintainer(s) if any
575     --n => include name 'Full Name <addr\@domain.tld>'
576     --l => include list(s) if any
577     --s => include subscriber only list(s) if any
578     --remove-duplicates => minimize duplicate email names/addresses
579     --roles => show roles (status:subsystem, git-signer, list, etc...)
580     --rolestats => show roles and statistics (commits/total_commits, %)
581     --file-emails => add email addresses found in -f file (default: 0 (off))
582   --scm => print SCM tree(s) if any
583   --status => print status if any
584   --subsystem => print subsystem name if any
585   --web => print website(s) if any
586
587 Output type options:
588   --separator [, ] => separator for multiple entries on 1 line
589     using --separator also sets --nomultiline if --separator is not [, ]
590   --multiline => print 1 entry per line
591
592 Other options:
593   --pattern-depth => Number of pattern directory traversals (default: 0 (all))
594   --keywords => scan patch for keywords (default: 1 (on))
595   --sections => print the entire subsystem sections with pattern matches
596   --version => show version
597   --help => show this help information
598
599 Default options:
600   [--email --git --m --n --l --multiline --pattern-depth=0 --remove-duplicates]
601
602 Notes:
603   Using "-f directory" may give unexpected results:
604       Used with "--git", git signators for _all_ files in and below
605           directory are examined as git recurses directories.
606           Any specified X: (exclude) pattern matches are _not_ ignored.
607       Used with "--nogit", directory is used as a pattern match,
608           no individual file within the directory or subdirectory
609           is matched.
610       Used with "--git-blame", does not iterate all files in directory
611   Using "--git-blame" is slow and may add old committers and authors
612       that are no longer active maintainers to the output.
613   Using "--roles" or "--rolestats" with git send-email --cc-cmd or any
614       other automated tools that expect only ["name"] <email address>
615       may not work because of additional output after <email address>.
616   Using "--rolestats" and "--git-blame" shows the #/total=% commits,
617       not the percentage of the entire file authored.  # of commits is
618       not a good measure of amount of code authored.  1 major commit may
619       contain a thousand lines, 5 trivial commits may modify a single line.
620   If git is not installed, but mercurial (hg) is installed and an .hg
621       repository exists, the following options apply to mercurial:
622           --git,
623           --git-min-signatures, --git-max-maintainers, --git-min-percent, and
624           --git-blame
625       Use --hg-since not --git-since to control date selection
626   File ".get_maintainer.conf", if it exists in the linux kernel source root
627       directory, can change whatever get_maintainer defaults are desired.
628       Entries in this file can be any command line argument.
629       This file is prepended to any additional command line arguments.
630       Multiple lines and # comments are allowed.
631 EOT
632 }
633
634 sub top_of_kernel_tree {
635         my ($lk_path) = @_;
636
637         if ($lk_path ne "" && substr($lk_path,length($lk_path)-1,1) ne "/") {
638             $lk_path .= "/";
639         }
640         if (   (-f "${lk_path}COPYING")
641             && (-f "${lk_path}CREDITS")
642             && (-f "${lk_path}Kbuild")
643             && (-f "${lk_path}MAINTAINERS")
644             && (-f "${lk_path}Makefile")
645             && (-f "${lk_path}README")
646             && (-d "${lk_path}Documentation")
647             && (-d "${lk_path}arch")
648             && (-d "${lk_path}include")
649             && (-d "${lk_path}drivers")
650             && (-d "${lk_path}fs")
651             && (-d "${lk_path}init")
652             && (-d "${lk_path}ipc")
653             && (-d "${lk_path}kernel")
654             && (-d "${lk_path}lib")
655             && (-d "${lk_path}scripts")) {
656                 return 1;
657         }
658         return 0;
659 }
660
661 sub parse_email {
662     my ($formatted_email) = @_;
663
664     my $name = "";
665     my $address = "";
666
667     if ($formatted_email =~ /^([^<]+)<(.+\@.*)>.*$/) {
668         $name = $1;
669         $address = $2;
670     } elsif ($formatted_email =~ /^\s*<(.+\@\S*)>.*$/) {
671         $address = $1;
672     } elsif ($formatted_email =~ /^(.+\@\S*).*$/) {
673         $address = $1;
674     }
675
676     $name =~ s/^\s+|\s+$//g;
677     $name =~ s/^\"|\"$//g;
678     $address =~ s/^\s+|\s+$//g;
679
680     if ($name =~ /[^\w \-]/i) {          ##has "must quote" chars
681         $name =~ s/(?<!\\)"/\\"/g;       ##escape quotes
682         $name = "\"$name\"";
683     }
684
685     return ($name, $address);
686 }
687
688 sub format_email {
689     my ($name, $address, $usename) = @_;
690
691     my $formatted_email;
692
693     $name =~ s/^\s+|\s+$//g;
694     $name =~ s/^\"|\"$//g;
695     $address =~ s/^\s+|\s+$//g;
696
697     if ($name =~ /[^\w \-]/i) {          ##has "must quote" chars
698         $name =~ s/(?<!\\)"/\\"/g;       ##escape quotes
699         $name = "\"$name\"";
700     }
701
702     if ($usename) {
703         if ("$name" eq "") {
704             $formatted_email = "$address";
705         } else {
706             $formatted_email = "$name <$address>";
707         }
708     } else {
709         $formatted_email = $address;
710     }
711
712     return $formatted_email;
713 }
714
715 sub find_first_section {
716     my $index = 0;
717
718     while ($index < @typevalue) {
719         my $tv = $typevalue[$index];
720         if (($tv =~ m/^(\C):\s*(.*)/)) {
721             last;
722         }
723         $index++;
724     }
725
726     return $index;
727 }
728
729 sub find_starting_index {
730     my ($index) = @_;
731
732     while ($index > 0) {
733         my $tv = $typevalue[$index];
734         if (!($tv =~ m/^(\C):\s*(.*)/)) {
735             last;
736         }
737         $index--;
738     }
739
740     return $index;
741 }
742
743 sub find_ending_index {
744     my ($index) = @_;
745
746     while ($index < @typevalue) {
747         my $tv = $typevalue[$index];
748         if (!($tv =~ m/^(\C):\s*(.*)/)) {
749             last;
750         }
751         $index++;
752     }
753
754     return $index;
755 }
756
757 sub get_maintainer_role {
758     my ($index) = @_;
759
760     my $i;
761     my $start = find_starting_index($index);
762     my $end = find_ending_index($index);
763
764     my $role;
765     my $subsystem = $typevalue[$start];
766     if (length($subsystem) > 20) {
767         $subsystem = substr($subsystem, 0, 17);
768         $subsystem =~ s/\s*$//;
769         $subsystem = $subsystem . "...";
770     }
771
772     for ($i = $start + 1; $i < $end; $i++) {
773         my $tv = $typevalue[$i];
774         if ($tv =~ m/^(\C):\s*(.*)/) {
775             my $ptype = $1;
776             my $pvalue = $2;
777             if ($ptype eq "S") {
778                 $role = $pvalue;
779             }
780         }
781     }
782
783     $role = lc($role);
784     if      ($role eq "supported") {
785         $role = "supporter";
786     } elsif ($role eq "maintained") {
787         $role = "maintainer";
788     } elsif ($role eq "odd fixes") {
789         $role = "odd fixer";
790     } elsif ($role eq "orphan") {
791         $role = "orphan minder";
792     } elsif ($role eq "obsolete") {
793         $role = "obsolete minder";
794     } elsif ($role eq "buried alive in reporters") {
795         $role = "chief penguin";
796     }
797
798     return $role . ":" . $subsystem;
799 }
800
801 sub get_list_role {
802     my ($index) = @_;
803
804     my $i;
805     my $start = find_starting_index($index);
806     my $end = find_ending_index($index);
807
808     my $subsystem = $typevalue[$start];
809     if (length($subsystem) > 20) {
810         $subsystem = substr($subsystem, 0, 17);
811         $subsystem =~ s/\s*$//;
812         $subsystem = $subsystem . "...";
813     }
814
815     if ($subsystem eq "THE REST") {
816         $subsystem = "";
817     }
818
819     return $subsystem;
820 }
821
822 sub add_categories {
823     my ($index) = @_;
824
825     my $i;
826     my $start = find_starting_index($index);
827     my $end = find_ending_index($index);
828
829     push(@subsystem, $typevalue[$start]);
830
831     for ($i = $start + 1; $i < $end; $i++) {
832         my $tv = $typevalue[$i];
833         if ($tv =~ m/^(\C):\s*(.*)/) {
834             my $ptype = $1;
835             my $pvalue = $2;
836             if ($ptype eq "L") {
837                 my $list_address = $pvalue;
838                 my $list_additional = "";
839                 my $list_role = get_list_role($i);
840
841                 if ($list_role ne "") {
842                     $list_role = ":" . $list_role;
843                 }
844                 if ($list_address =~ m/([^\s]+)\s+(.*)$/) {
845                     $list_address = $1;
846                     $list_additional = $2;
847                 }
848                 if ($list_additional =~ m/subscribers-only/) {
849                     if ($email_subscriber_list) {
850                         push(@list_to, [$list_address, "subscriber list${list_role}"]);
851                     }
852                 } else {
853                     if ($email_list) {
854                         push(@list_to, [$list_address, "open list${list_role}"]);
855                     }
856                 }
857             } elsif ($ptype eq "M") {
858                 my ($name, $address) = parse_email($pvalue);
859                 if ($name eq "") {
860                     if ($i > 0) {
861                         my $tv = $typevalue[$i - 1];
862                         if ($tv =~ m/^(\C):\s*(.*)/) {
863                             if ($1 eq "P") {
864                                 $name = $2;
865                                 $pvalue = format_email($name, $address, $email_usename);
866                             }
867                         }
868                     }
869                 }
870                 if ($email_maintainer) {
871                     my $role = get_maintainer_role($i);
872                     push_email_addresses($pvalue, $role);
873                 }
874             } elsif ($ptype eq "T") {
875                 push(@scm, $pvalue);
876             } elsif ($ptype eq "W") {
877                 push(@web, $pvalue);
878             } elsif ($ptype eq "S") {
879                 push(@status, $pvalue);
880             }
881         }
882     }
883 }
884
885 my %email_hash_name;
886 my %email_hash_address;
887
888 sub email_inuse {
889     my ($name, $address) = @_;
890
891     return 1 if (($name eq "") && ($address eq ""));
892     return 1 if (($name ne "") && exists($email_hash_name{$name}));
893     return 1 if (($address ne "") && exists($email_hash_address{$address}));
894
895     return 0;
896 }
897
898 sub push_email_address {
899     my ($line, $role) = @_;
900
901     my ($name, $address) = parse_email($line);
902
903     if ($address eq "") {
904         return 0;
905     }
906
907     if (!$email_remove_duplicates) {
908         push(@email_to, [format_email($name, $address, $email_usename), $role]);
909     } elsif (!email_inuse($name, $address)) {
910         push(@email_to, [format_email($name, $address, $email_usename), $role]);
911         $email_hash_name{$name}++;
912         $email_hash_address{$address}++;
913     }
914
915     return 1;
916 }
917
918 sub push_email_addresses {
919     my ($address, $role) = @_;
920
921     my @address_list = ();
922
923     if (rfc822_valid($address)) {
924         push_email_address($address, $role);
925     } elsif (@address_list = rfc822_validlist($address)) {
926         my $array_count = shift(@address_list);
927         while (my $entry = shift(@address_list)) {
928             push_email_address($entry, $role);
929         }
930     } else {
931         if (!push_email_address($address, $role)) {
932             warn("Invalid MAINTAINERS address: '" . $address . "'\n");
933         }
934     }
935 }
936
937 sub add_role {
938     my ($line, $role) = @_;
939
940     my ($name, $address) = parse_email($line);
941     my $email = format_email($name, $address, $email_usename);
942
943     foreach my $entry (@email_to) {
944         if ($email_remove_duplicates) {
945             my ($entry_name, $entry_address) = parse_email($entry->[0]);
946             if (($name eq $entry_name || $address eq $entry_address)
947                 && ($role eq "" || !($entry->[1] =~ m/$role/))
948             ) {
949                 if ($entry->[1] eq "") {
950                     $entry->[1] = "$role";
951                 } else {
952                     $entry->[1] = "$entry->[1],$role";
953                 }
954             }
955         } else {
956             if ($email eq $entry->[0]
957                 && ($role eq "" || !($entry->[1] =~ m/$role/))
958             ) {
959                 if ($entry->[1] eq "") {
960                     $entry->[1] = "$role";
961                 } else {
962                     $entry->[1] = "$entry->[1],$role";
963                 }
964             }
965         }
966     }
967 }
968
969 sub which {
970     my ($bin) = @_;
971
972     foreach my $path (split(/:/, $ENV{PATH})) {
973         if (-e "$path/$bin") {
974             return "$path/$bin";
975         }
976     }
977
978     return "";
979 }
980
981 sub which_conf {
982     my ($conf) = @_;
983
984     foreach my $path (split(/:/, ".:$ENV{HOME}:.scripts")) {
985         if (-e "$path/$conf") {
986             return "$path/$conf";
987         }
988     }
989
990     return "";
991 }
992
993 sub mailmap {
994     my (@lines) = @_;
995     my %hash;
996
997     foreach my $line (@lines) {
998         my ($name, $address) = parse_email($line);
999         if (!exists($hash{$name})) {
1000             $hash{$name} = $address;
1001         } elsif ($address ne $hash{$name}) {
1002             $address = $hash{$name};
1003             $line = format_email($name, $address, $email_usename);
1004         }
1005         if (exists($mailmap{$name})) {
1006             my $obj = $mailmap{$name};
1007             foreach my $map_address (@$obj) {
1008                 if (($map_address eq $address) &&
1009                     ($map_address ne $hash{$name})) {
1010                     $line = format_email($name, $hash{$name}, $email_usename);
1011                 }
1012             }
1013         }
1014     }
1015
1016     return @lines;
1017 }
1018
1019 sub git_execute_cmd {
1020     my ($cmd) = @_;
1021     my @lines = ();
1022
1023     my $output = `$cmd`;
1024     $output =~ s/^\s*//gm;
1025     @lines = split("\n", $output);
1026
1027     return @lines;
1028 }
1029
1030 sub hg_execute_cmd {
1031     my ($cmd) = @_;
1032     my @lines = ();
1033
1034     my $output = `$cmd`;
1035     @lines = split("\n", $output);
1036
1037     return @lines;
1038 }
1039
1040 sub vcs_find_signers {
1041     my ($cmd) = @_;
1042     my @lines = ();
1043     my $commits;
1044
1045     @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1046
1047     my $pattern = $VCS_cmds{"commit_pattern"};
1048
1049     $commits = grep(/$pattern/, @lines);        # of commits
1050
1051     @lines = grep(/^[ \t]*${signaturePattern}.*\@.*$/, @lines);
1052     if (!$email_git_penguin_chiefs) {
1053         @lines = grep(!/${penguin_chiefs}/i, @lines);
1054     }
1055
1056     return (0, @lines) if !@lines;
1057
1058     # cut -f2- -d":"
1059     s/.*:\s*(.+)\s*/$1/ for (@lines);
1060
1061 ## Reformat email addresses (with names) to avoid badly written signatures
1062
1063     foreach my $line (@lines) {
1064         my ($name, $address) = parse_email($line);
1065         $line = format_email($name, $address, 1);
1066     }
1067
1068     return ($commits, @lines);
1069 }
1070
1071 sub vcs_find_author {
1072     my ($cmd) = @_;
1073     my @lines = ();
1074
1075     @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1076
1077     if (!$email_git_penguin_chiefs) {
1078         @lines = grep(!/${penguin_chiefs}/i, @lines);
1079     }
1080
1081     return @lines if !@lines;
1082
1083 ## Reformat email addresses (with names) to avoid badly written signatures
1084
1085     foreach my $line (@lines) {
1086         my ($name, $address) = parse_email($line);
1087         $line = format_email($name, $address, 1);
1088     }
1089
1090     return @lines;
1091 }
1092
1093 sub vcs_save_commits {
1094     my ($cmd) = @_;
1095     my @lines = ();
1096     my @commits = ();
1097
1098     @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1099
1100     foreach my $line (@lines) {
1101         if ($line =~ m/$VCS_cmds{"blame_commit_pattern"}/) {
1102             push(@commits, $1);
1103         }
1104     }
1105
1106     return @commits;
1107 }
1108
1109 sub vcs_blame {
1110     my ($file) = @_;
1111     my $cmd;
1112     my @commits = ();
1113
1114     return @commits if (!(-f $file));
1115
1116     if (@range && $VCS_cmds{"blame_range_cmd"} eq "") {
1117         my @all_commits = ();
1118
1119         $cmd = $VCS_cmds{"blame_file_cmd"};
1120         $cmd =~ s/(\$\w+)/$1/eeg;               #interpolate $cmd
1121         @all_commits = vcs_save_commits($cmd);
1122
1123         foreach my $file_range_diff (@range) {
1124             next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1125             my $diff_file = $1;
1126             my $diff_start = $2;
1127             my $diff_length = $3;
1128             next if ("$file" ne "$diff_file");
1129             for (my $i = $diff_start; $i < $diff_start + $diff_length; $i++) {
1130                 push(@commits, $all_commits[$i]);
1131             }
1132         }
1133     } elsif (@range) {
1134         foreach my $file_range_diff (@range) {
1135             next if (!($file_range_diff =~ m/(.+):(.+):(.+)/));
1136             my $diff_file = $1;
1137             my $diff_start = $2;
1138             my $diff_length = $3;
1139             next if ("$file" ne "$diff_file");
1140             $cmd = $VCS_cmds{"blame_range_cmd"};
1141             $cmd =~ s/(\$\w+)/$1/eeg;           #interpolate $cmd
1142             push(@commits, vcs_save_commits($cmd));
1143         }
1144     } else {
1145         $cmd = $VCS_cmds{"blame_file_cmd"};
1146         $cmd =~ s/(\$\w+)/$1/eeg;               #interpolate $cmd
1147         @commits = vcs_save_commits($cmd);
1148     }
1149
1150     foreach my $commit (@commits) {
1151         $commit =~ s/^\^//g;
1152     }
1153
1154     return @commits;
1155 }
1156
1157 my $printed_novcs = 0;
1158 sub vcs_exists {
1159     %VCS_cmds = %VCS_cmds_git;
1160     return 1 if eval $VCS_cmds{"available"};
1161     %VCS_cmds = %VCS_cmds_hg;
1162     return 1 if eval $VCS_cmds{"available"};
1163     %VCS_cmds = ();
1164     if (!$printed_novcs) {
1165         warn("$P: No supported VCS found.  Add --nogit to options?\n");
1166         warn("Using a git repository produces better results.\n");
1167         warn("Try Linus Torvalds' latest git repository using:\n");
1168         warn("git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git\n");
1169         $printed_novcs = 1;
1170     }
1171     return 0;
1172 }
1173
1174 sub vcs_interactive_menu {
1175     my $list_ref = shift;
1176     my @list = @$list_ref;
1177
1178     return if (!vcs_exists());
1179
1180     my %selected;
1181     my %shortlog;
1182     my $input;
1183     my $count = 0;
1184
1185     #select maintainers by default
1186     foreach my $entry (@list){
1187             my $role = $entry->[1];
1188             $selected{$count} = ($role =~ /maintainer:|supporter:/);
1189             $count++;
1190     }
1191
1192     #menu loop
1193     do {
1194         my $count = 0;
1195         foreach my $entry (@list){
1196             my $email = $entry->[0];
1197             my $role = $entry->[1];
1198             if ($selected{$count}){
1199                 print STDERR "* ";
1200             } else {
1201                 print STDERR "  ";
1202             }
1203             print STDERR "$count: $email,\t\t $role";
1204             print STDERR "\n";
1205             if ($shortlog{$count}){
1206                 my $entries_ref = vcs_get_shortlog($email);
1207                 foreach my $entry_ref (@{$entries_ref}){
1208                     my $filename = @{$entry_ref}[0];
1209                     my @shortlog = @{@{$entry_ref}[1]};
1210                     print STDERR "\tshortlog for $filename (authored commits: " . @shortlog . ").\n";
1211                     foreach my $commit (@shortlog){
1212                         print STDERR "\t  $commit\n";
1213                     }
1214                     print STDERR "\n";
1215                 }
1216             }
1217             $count++;
1218         }
1219         print STDERR "\n";
1220         print STDERR "Choose whom to cc by entering a commaseperated list of numbers and hitting enter.\n";
1221         print STDERR "To show a short list of commits, precede the number by a '?',\n";
1222         print STDERR "A blank line indicates that you are satisfied with your choice.\n";
1223         $input = <STDIN>;
1224         chomp($input);
1225
1226         my @wish = split(/[, ]+/,$input);
1227         foreach my $nr (@wish){
1228                 my $logtoggle = 0;
1229                 if ($nr =~ /\?/){
1230                         $nr =~ s/\?//;
1231                         $logtoggle = 1;
1232                 }
1233
1234                 #skip out of bounds numbers
1235                 next unless ($nr <= $count && $nr >= 0);
1236
1237                 if ($logtoggle){
1238                         $shortlog{$nr} = !$shortlog{$nr};
1239                 } else {
1240                         $selected{$nr} = !$selected{$nr};
1241
1242                         #switch shortlog on if an entry get's selected
1243                         if ($selected{$nr}){
1244                                 $shortlog{$nr}=1;
1245                         }
1246                 }
1247         };
1248     } while(length($input) > 0);
1249
1250     #drop not selected entries
1251     $count = 0;
1252     my @new_emailto;
1253     foreach my $entry (@list){
1254         if ($selected{$count}){
1255                 push(@new_emailto,$list[$count]);
1256                 print STDERR "$count: ";
1257                 print STDERR $email_to[$count]->[0];
1258                 print STDERR ",\t\t ";
1259                 print STDERR $email_to[$count]->[1];
1260                 print STDERR "\n";
1261         }
1262         $count++;
1263     }
1264     return \@new_emailto;
1265 }
1266
1267 sub vcs_get_shortlog {
1268     my $arg = shift;
1269     my ($name, $address) = parse_email($arg);
1270     return $shortlog_buffer{$address};
1271 }
1272
1273 sub vcs_file_shortlogs {
1274     my ($file) = @_;
1275     print STDERR "shortlog processing $file:";
1276     foreach my $entry (@email_to){
1277         my ($name, $address) = parse_email($entry->[0]);
1278         print STDERR ".";
1279         my $commits_ref = vcs_email_shortlog($address, $file);
1280         push(@{$shortlog_buffer{$address}}, [ $file, $commits_ref ]);
1281     }
1282     print STDERR "\n";
1283 }
1284
1285 sub vcs_email_shortlog {
1286     my $email = shift;
1287     my ($file) = @_;
1288
1289     my $cmd = $VCS_cmds{"shortlog_cmd"};
1290     $cmd =~ s/(\$\w+)/$1/eeg;           #substitute variables
1291     my @lines = &{$VCS_cmds{"execute_cmd"}}($cmd);
1292     return \@lines;
1293 }
1294
1295 sub vcs_assign {
1296     my ($role, $divisor, @lines) = @_;
1297
1298     my %hash;
1299     my $count = 0;
1300
1301     return if (@lines <= 0);
1302
1303     if ($divisor <= 0) {
1304         warn("Bad divisor in " . (caller(0))[3] . ": $divisor\n");
1305         $divisor = 1;
1306     }
1307
1308     if ($email_remove_duplicates) {
1309         @lines = mailmap(@lines);
1310     }
1311
1312     return if (@lines <= 0);
1313
1314     @lines = sort(@lines);
1315
1316     # uniq -c
1317     $hash{$_}++ for @lines;
1318
1319     # sort -rn
1320     foreach my $line (sort {$hash{$b} <=> $hash{$a}} keys %hash) {
1321         my $sign_offs = $hash{$line};
1322         my $percent = $sign_offs * 100 / $divisor;
1323
1324         $percent = 100 if ($percent > 100);
1325         $count++;
1326         last if ($sign_offs < $email_git_min_signatures ||
1327                  $count > $email_git_max_maintainers ||
1328                  $percent < $email_git_min_percent);
1329         push_email_address($line, '');
1330         if ($output_rolestats) {
1331             my $fmt_percent = sprintf("%.0f", $percent);
1332             add_role($line, "$role:$sign_offs/$divisor=$fmt_percent%");
1333         } else {
1334             add_role($line, $role);
1335         }
1336     }
1337 }
1338
1339 sub vcs_file_signoffs {
1340     my ($file) = @_;
1341
1342     my @signers = ();
1343     my $commits;
1344
1345     return if (!vcs_exists());
1346
1347     my $cmd = $VCS_cmds{"find_signers_cmd"};
1348     $cmd =~ s/(\$\w+)/$1/eeg;           # interpolate $cmd
1349
1350     ($commits, @signers) = vcs_find_signers($cmd);
1351     vcs_assign("commit_signer", $commits, @signers);
1352 }
1353
1354 sub vcs_file_blame {
1355     my ($file) = @_;
1356
1357     my @signers = ();
1358     my @all_commits = ();
1359     my @commits = ();
1360     my $total_commits;
1361     my $total_lines;
1362
1363     return if (!vcs_exists());
1364
1365     @all_commits = vcs_blame($file);
1366     @commits = uniq(@all_commits);
1367     $total_commits = @commits;
1368     $total_lines = @all_commits;
1369
1370     foreach my $commit (@commits) {
1371         my $commit_count;
1372         my @commit_signers = ();
1373
1374         my $cmd = $VCS_cmds{"find_commit_signers_cmd"};
1375         $cmd =~ s/(\$\w+)/$1/eeg;       #substitute variables in $cmd
1376
1377         ($commit_count, @commit_signers) = vcs_find_signers($cmd);
1378
1379         push(@signers, @commit_signers);
1380     }
1381
1382     if ($from_filename) {
1383         if ($output_rolestats) {
1384             my @blame_signers;
1385             foreach my $commit (@commits) {
1386                 my $i;
1387                 my $cmd = $VCS_cmds{"find_commit_author_cmd"};
1388                 $cmd =~ s/(\$\w+)/$1/eeg;       #interpolate $cmd
1389                 my @author = vcs_find_author($cmd);
1390                 next if !@author;
1391                 my $count = grep(/$commit/, @all_commits);
1392                 for ($i = 0; $i < $count ; $i++) {
1393                     push(@blame_signers, $author[0]);
1394                 }
1395             }
1396             if (@blame_signers) {
1397                 vcs_assign("authored lines", $total_lines, @blame_signers);
1398             }
1399         }
1400         vcs_assign("commits", $total_commits, @signers);
1401     } else {
1402         vcs_assign("modified commits", $total_commits, @signers);
1403     }
1404 }
1405
1406 sub uniq {
1407     my (@parms) = @_;
1408
1409     my %saw;
1410     @parms = grep(!$saw{$_}++, @parms);
1411     return @parms;
1412 }
1413
1414 sub sort_and_uniq {
1415     my (@parms) = @_;
1416
1417     my %saw;
1418     @parms = sort @parms;
1419     @parms = grep(!$saw{$_}++, @parms);
1420     return @parms;
1421 }
1422
1423 sub clean_file_emails {
1424     my (@file_emails) = @_;
1425     my @fmt_emails = ();
1426
1427     foreach my $email (@file_emails) {
1428         $email =~ s/[\(\<\{]{0,1}([A-Za-z0-9_\.\+-]+\@[A-Za-z0-9\.-]+)[\)\>\}]{0,1}/\<$1\>/g;
1429         my ($name, $address) = parse_email($email);
1430         if ($name eq '"[,\.]"') {
1431             $name = "";
1432         }
1433
1434         my @nw = split(/[^A-Za-zÀ-ÿ\'\,\.\+-]/, $name);
1435         if (@nw > 2) {
1436             my $first = $nw[@nw - 3];
1437             my $middle = $nw[@nw - 2];
1438             my $last = $nw[@nw - 1];
1439
1440             if (((length($first) == 1 && $first =~ m/[A-Za-z]/) ||
1441                  (length($first) == 2 && substr($first, -1) eq ".")) ||
1442                 (length($middle) == 1 ||
1443                  (length($middle) == 2 && substr($middle, -1) eq "."))) {
1444                 $name = "$first $middle $last";
1445             } else {
1446                 $name = "$middle $last";
1447             }
1448         }
1449
1450         if (substr($name, -1) =~ /[,\.]/) {
1451             $name = substr($name, 0, length($name) - 1);
1452         } elsif (substr($name, -2) =~ /[,\.]"/) {
1453             $name = substr($name, 0, length($name) - 2) . '"';
1454         }
1455
1456         if (substr($name, 0, 1) =~ /[,\.]/) {
1457             $name = substr($name, 1, length($name) - 1);
1458         } elsif (substr($name, 0, 2) =~ /"[,\.]/) {
1459             $name = '"' . substr($name, 2, length($name) - 2);
1460         }
1461
1462         my $fmt_email = format_email($name, $address, $email_usename);
1463         push(@fmt_emails, $fmt_email);
1464     }
1465     return @fmt_emails;
1466 }
1467
1468 sub merge_email {
1469     my @lines;
1470     my %saw;
1471
1472     for (@_) {
1473         my ($address, $role) = @$_;
1474         if (!$saw{$address}) {
1475             if ($output_roles) {
1476                 push(@lines, "$address ($role)");
1477             } else {
1478                 push(@lines, $address);
1479             }
1480             $saw{$address} = 1;
1481         }
1482     }
1483
1484     return @lines;
1485 }
1486
1487 sub output {
1488     my (@parms) = @_;
1489
1490     if ($output_multiline) {
1491         foreach my $line (@parms) {
1492             print("${line}\n");
1493         }
1494     } else {
1495         print(join($output_separator, @parms));
1496         print("\n");
1497     }
1498 }
1499
1500 my $rfc822re;
1501
1502 sub make_rfc822re {
1503 #   Basic lexical tokens are specials, domain_literal, quoted_string, atom, and
1504 #   comment.  We must allow for rfc822_lwsp (or comments) after each of these.
1505 #   This regexp will only work on addresses which have had comments stripped
1506 #   and replaced with rfc822_lwsp.
1507
1508     my $specials = '()<>@,;:\\\\".\\[\\]';
1509     my $controls = '\\000-\\037\\177';
1510
1511     my $dtext = "[^\\[\\]\\r\\\\]";
1512     my $domain_literal = "\\[(?:$dtext|\\\\.)*\\]$rfc822_lwsp*";
1513
1514     my $quoted_string = "\"(?:[^\\\"\\r\\\\]|\\\\.|$rfc822_lwsp)*\"$rfc822_lwsp*";
1515
1516 #   Use zero-width assertion to spot the limit of an atom.  A simple
1517 #   $rfc822_lwsp* causes the regexp engine to hang occasionally.
1518     my $atom = "[^$specials $controls]+(?:$rfc822_lwsp+|\\Z|(?=[\\[\"$specials]))";
1519     my $word = "(?:$atom|$quoted_string)";
1520     my $localpart = "$word(?:\\.$rfc822_lwsp*$word)*";
1521
1522     my $sub_domain = "(?:$atom|$domain_literal)";
1523     my $domain = "$sub_domain(?:\\.$rfc822_lwsp*$sub_domain)*";
1524
1525     my $addr_spec = "$localpart\@$rfc822_lwsp*$domain";
1526
1527     my $phrase = "$word*";
1528     my $route = "(?:\@$domain(?:,\@$rfc822_lwsp*$domain)*:$rfc822_lwsp*)";
1529     my $route_addr = "\\<$rfc822_lwsp*$route?$addr_spec\\>$rfc822_lwsp*";
1530     my $mailbox = "(?:$addr_spec|$phrase$route_addr)";
1531
1532     my $group = "$phrase:$rfc822_lwsp*(?:$mailbox(?:,\\s*$mailbox)*)?;\\s*";
1533     my $address = "(?:$mailbox|$group)";
1534
1535     return "$rfc822_lwsp*$address";
1536 }
1537
1538 sub rfc822_strip_comments {
1539     my $s = shift;
1540 #   Recursively remove comments, and replace with a single space.  The simpler
1541 #   regexps in the Email Addressing FAQ are imperfect - they will miss escaped
1542 #   chars in atoms, for example.
1543
1544     while ($s =~ s/^((?:[^"\\]|\\.)*
1545                     (?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)
1546                     \((?:[^()\\]|\\.)*\)/$1 /osx) {}
1547     return $s;
1548 }
1549
1550 #   valid: returns true if the parameter is an RFC822 valid address
1551 #
1552 sub rfc822_valid {
1553     my $s = rfc822_strip_comments(shift);
1554
1555     if (!$rfc822re) {
1556         $rfc822re = make_rfc822re();
1557     }
1558
1559     return $s =~ m/^$rfc822re$/so && $s =~ m/^$rfc822_char*$/;
1560 }
1561
1562 #   validlist: In scalar context, returns true if the parameter is an RFC822
1563 #              valid list of addresses.
1564 #
1565 #              In list context, returns an empty list on failure (an invalid
1566 #              address was found); otherwise a list whose first element is the
1567 #              number of addresses found and whose remaining elements are the
1568 #              addresses.  This is needed to disambiguate failure (invalid)
1569 #              from success with no addresses found, because an empty string is
1570 #              a valid list.
1571
1572 sub rfc822_validlist {
1573     my $s = rfc822_strip_comments(shift);
1574
1575     if (!$rfc822re) {
1576         $rfc822re = make_rfc822re();
1577     }
1578     # * null list items are valid according to the RFC
1579     # * the '1' business is to aid in distinguishing failure from no results
1580
1581     my @r;
1582     if ($s =~ m/^(?:$rfc822re)?(?:,(?:$rfc822re)?)*$/so &&
1583         $s =~ m/^$rfc822_char*$/) {
1584         while ($s =~ m/(?:^|,$rfc822_lwsp*)($rfc822re)/gos) {
1585             push(@r, $1);
1586         }
1587         return wantarray ? (scalar(@r), @r) : 1;
1588     }
1589     return wantarray ? () : 0;
1590 }