xref: /curl/scripts/managen (revision 23e6391c)
1#!/usr/bin/env perl
2#***************************************************************************
3#                                  _   _ ____  _
4#  Project                     ___| | | |  _ \| |
5#                             / __| | | | |_) | |
6#                            | (__| |_| |  _ <| |___
7#                             \___|\___/|_| \_\_____|
8#
9# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
10#
11# This software is licensed as described in the file COPYING, which
12# you should have received as part of this distribution. The terms
13# are also available at https://curl.se/docs/copyright.html.
14#
15# You may opt to use, copy, modify, merge, publish, distribute and/or sell
16# copies of the Software, and permit persons to whom the Software is
17# furnished to do so, under the terms of the COPYING file.
18#
19# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
20# KIND, either express or implied.
21#
22# SPDX-License-Identifier: curl
23#
24###########################################################################
25
26=begin comment
27
28This script generates the manpage.
29
30Example: managen <command> [files] > curl.1
31
32Dev notes:
33
34We open *input* files in :crlf translation (a no-op on many platforms) in
35case we have CRLF line endings in Windows but a perl that defaults to LF.
36Unfortunately it seems some perls like msysgit cannot handle a global input-only
37:crlf so it has to be specified on each file open for text input.
38
39=end comment
40=cut
41
42my %optshort;
43my %optlong;
44my %helplong;
45my %arglong;
46my %redirlong;
47my %protolong;
48my %catlong;
49
50use POSIX qw(strftime);
51my @ts;
52if (defined($ENV{SOURCE_DATE_EPOCH})) {
53    @ts = gmtime($ENV{SOURCE_DATE_EPOCH});
54} else {
55    @ts = localtime;
56}
57my $date = strftime "%Y-%m-%d", @ts;
58my $year = strftime "%Y", @ts;
59my $version = "unknown";
60my $globals;
61my $error = 0;
62my $indent = 4;
63
64# get the long name version, return the manpage string
65sub manpageify {
66    my ($k)=@_;
67    my $l;
68    my $trail;
69    # the matching pattern might include a trailing dot that cannot be part of
70    # the option name
71    if($k =~ s/\.$//) {
72        # cut off trailing dot
73        $trail = ".";
74    }
75    my $klong = $k;
76    # quote "bare" minuses in the long name
77    $klong =~ s/-/\\-/g;
78    if($optlong{$k}) {
79        # both short + long
80        $l = "\\fI-".$optlong{$k}.", \\-\\-$klong\\fP$trail";
81    }
82    else {
83        # only long
84        $l = "\\fI\\-\\-$klong\\fP$trail";
85    }
86    return $l;
87}
88
89
90my $colwidth=79; # max number of columns
91
92sub prefixline {
93    my ($num) = @_;
94    print "\t" x ($num/8);
95    print ' ' x ($num%8);
96}
97
98sub justline {
99    my ($lvl, @line) = @_;
100    my $w = -1;
101    my $spaces = -1;
102    my $width = $colwidth - ($lvl * $indent);
103    for(@line) {
104        $w += length($_);
105        $w++;
106        $spaces++;
107    }
108    my $inject = $width - $w;
109    my $ratio = 0; # stay at zero if no spaces at all
110    if($spaces) {
111        $ratio = $inject / $spaces;
112    }
113    my $spare = 0;
114    prefixline($lvl * $indent);
115    my $prev;
116    for(@line) {
117        while($spare >= 0.90) {
118            print " ";
119            $spare--;
120        }
121        printf "%s%s", $prev?" ":"", $_;
122        $prev = 1;
123        $spare += $ratio;
124    }
125    print "\n";
126}
127
128sub lastline {
129    my ($lvl, @line) = @_;
130    $line[0] =~ s/^( +)//;
131    prefixline($lvl * $indent + length($1));
132    my $prev = 0;
133    for(@line) {
134        printf "%s%s", $prev?" ":"", $_;
135        $prev = 1;
136    }
137    print "\n";
138}
139
140sub outputpara {
141    my ($lvl, $f) = @_;
142    $f =~ s/\n/ /g;
143
144    my $w = 0;
145    my @words = split(/  */, $f);
146    my $width = $colwidth - ($lvl * $indent);
147
148    my @line;
149    for my $e (@words) {
150        my $l = length($e);
151        my $spaces = scalar(@line);
152        if(($w + $l + $spaces) >= $width) {
153            justline($lvl, @line);
154            undef @line;
155            $w = 0;
156        }
157
158        push @line, $e;
159        $w += $l; # new width
160    }
161    if($w) {
162        lastline($lvl, @line);
163        print "\n";
164    }
165}
166
167sub printdesc {
168    my ($manpage, $baselvl, @desc) = @_;
169
170    if($manpage) {
171        for my $d (@desc) {
172            print $d;
173        }
174    }
175    else {
176        my $p = -1;
177        my $para;
178        for my $l (@desc) {
179            my $lvl;
180            if($l !~ /^[\n\r]+/) {
181                # get the indent level off the string
182                $l =~ s/^\[([0-9q]*)\]//;
183                $lvl = $1;
184            }
185            if(($p =~ /q/) && ($lvl !~ /q/)) {
186                # the previous was quoted, this is not
187                print "\n";
188            }
189            if($lvl != $p) {
190                outputpara($baselvl + $p, $para);
191                $para = "";
192            }
193            if($lvl =~ /q/) {
194                # quoted, do not right-justify
195                chomp $l;
196                lastline($baselvl + $lvl + 1, $l);
197                my $w = ($baselvl + $lvl + 1) * $indent + length($l);
198                if ($w > $colwidth) {
199                    print STDERR "ERROR: $w columns is too long\n";
200                    print STDERR "$l\n";
201                    $error++;
202                }
203            }
204            else {
205                $para .= $l;
206            }
207
208            $p = $lvl;
209        }
210        outputpara($baselvl + $p, $para);
211    }
212}
213
214sub seealso {
215    my($standalone, $data)=@_;
216    if($standalone) {
217        return sprintf
218            ".SH \"SEE ALSO\"\n$data\n";
219    }
220    else {
221        return "See also $data. ";
222    }
223}
224
225sub overrides {
226    my ($standalone, $data)=@_;
227    if($standalone) {
228        return ".SH \"OVERRIDES\"\n$data\n";
229    }
230    else {
231        return $data;
232    }
233}
234
235sub protocols {
236    my ($manpage, $standalone, $data)=@_;
237    if($standalone) {
238        return ".SH \"PROTOCOLS\"\n$data\n";
239    }
240    else {
241        return "($data) " if($manpage);
242        return "[1]($data) " if(!$manpage);
243    }
244}
245
246sub too_old {
247    my ($version)=@_;
248    my $a = 999999;
249    if($version =~ /^(\d+)\.(\d+)\.(\d+)/) {
250        $a = $1 * 1000 + $2 * 10 + $3;
251    }
252    elsif($version =~ /^(\d+)\.(\d+)/) {
253        $a = $1 * 1000 + $2 * 10;
254    }
255    if($a < 7600) {
256        # we consider everything before 7.60.0 to be too old to mention
257        # specific changes for
258        return 1;
259    }
260    return 0;
261}
262
263sub added {
264    my ($standalone, $data)=@_;
265    if(too_old($data)) {
266        # do not mention ancient additions
267        return "";
268    }
269    if($standalone) {
270        return ".SH \"ADDED\"\nAdded in curl version $data\n";
271    }
272    else {
273        return "Added in $data. ";
274    }
275}
276
277sub render {
278    my ($manpage, $fh, $f, $line) = @_;
279    my @desc;
280    my $tablemode = 0;
281    my $header = 0;
282    # if $top is TRUE, it means a top-level page and not a command line option
283    my $top = ($line == 1);
284    my $quote;
285    my $level;
286    my $finalblank;
287    $start = 0;
288
289    while(<$fh>) {
290        my $d = $_;
291        $line++;
292        $finalblank = ($d eq "\n");
293        if($d =~ /^\.(SH|BR|IP|B)/) {
294            print STDERR "$f:$line:1:ERROR: nroff instruction in input: \".$1\"\n";
295            return 4;
296        }
297        if(/^ *<!--/) {
298            # skip comments
299            next;
300        }
301        if((!$start) && ($_ =~ /^[\r\n]*\z/)) {
302            # skip leading blank lines
303            next;
304        }
305        $start = 1;
306        if(/^# (.*)/) {
307            $header = 1;
308            if($top != 1) {
309                # ignored for command line options
310                $blankline++;
311                next;
312            }
313            push @desc, ".SH $1\n" if($manpage);
314            push @desc, "[0]$1\n" if(!$manpage);
315            next;
316        }
317        elsif(/^###/) {
318            print STDERR "$f:$line:1:ERROR: ### header is not supported\n";
319            exit 3;
320        }
321        elsif(/^## (.*)/) {
322            my $word = $1;
323            # if there are enclosing quotes, remove them first
324            $word =~ s/[\"\'](.*)[\"\']\z/$1/;
325
326            # remove backticks from headers
327            $word =~ s/\`//g;
328
329            # if there is a space, it needs quotes for manpage
330            if(($word =~ / /) && $manpage) {
331                $word = "\"$word\"";
332            }
333            $level = 1;
334            if($top == 1) {
335                push @desc, ".IP $word\n" if($manpage);
336                push @desc, "\n" if(!$manpage);
337                push @desc, "[1]$word\n" if(!$manpage);
338            }
339            else {
340                if(!$tablemode) {
341                    push @desc, ".RS\n" if($manpage);
342                    $tablemode = 1;
343                }
344                push @desc, ".IP $word\n" if($manpage);
345                push @desc, "\n" if(!$manpage);
346                push @desc, "[1]$word\n" if(!$manpage);
347            }
348            $header = 1;
349            next;
350        }
351        elsif(/^##/) {
352            if($top == 1) {
353                print STDERR "$f:$line:1:ERROR: ## empty header top-level mode\n";
354                exit 3;
355            }
356            if($tablemode) {
357                # end of table
358                push @desc, ".RE\n.IP\n" if($manpage);
359                $tablemode = 0;
360            }
361            $header = 1;
362            next;
363        }
364        elsif(/^\.(IP|RS|RE)/) {
365            my ($cmd) = ($1);
366            print STDERR "$f:$line:1:ERROR: $cmd detected, use ##-style\n";
367            return 3;
368        }
369        elsif(/^[ \t]*\n/) {
370            # count and ignore blank lines
371            $blankline++;
372            next;
373        }
374        elsif($d =~ /^    (.*)/) {
375            my $word = $1;
376            if(!$quote && $manpage) {
377                push @desc, "\n" if($blankline);
378                push @desc, ".nf\n";
379                $blankline = 0;
380            }
381            $quote = 1;
382            $d = "$word\n";
383        }
384        elsif($quote && ($d !~ /^    (.*)/)) {
385            # end of quote
386            push @desc, ".fi\n" if($manpage);
387            $quote = 0;
388        }
389
390        $d =~ s/`%DATE`/$date/g;
391        $d =~ s/`%VERSION`/$version/g;
392        $d =~ s/`%GLOBALS`/$globals/g;
393
394        # convert backticks to double quotes
395        $d =~ s/\`/\"/g;
396
397        if($d =~ /\(added in(.*)/i) {
398            if(length($1) < 2) {
399                print STDERR "$f:$line:1:ERROR: broken up added-in line:\n";
400                print STDERR "$f:$line:1:ERROR: $d";
401                return 3;
402            }
403        }
404      again:
405        if($d =~ /\(Added in ([0-9.]+)\)/i) {
406            my $ver = $1;
407            if(too_old($ver)) {
408                $d =~ s/ *\(Added in $ver\)//gi;
409                goto again;
410            }
411        }
412
413        if(!$quote) {
414            if($d =~ /^(.*)  /) {
415                printf STDERR "$f:$line:%d:ERROR: 2 spaces detected\n",
416                    length($1);
417                return 3;
418            }
419            elsif($d =~ /[^\\][\<\>]/) {
420                print STDERR "$f:$line:1:WARN: un-escaped < or > used\n";
421                return 3;
422            }
423        }
424        # convert backslash-'<' or '> to just the second character
425        $d =~ s/\\([><])/$1/g;
426        # convert single backslash to double-backslash
427        $d =~ s/\\/\\\\/g if($manpage);
428
429
430        if($manpage) {
431            if(!$quote && $d =~ /--/) {
432                $d =~ s/--([a-z0-9.-]+)/manpageify($1)/ge;
433            }
434
435            # quote minuses in the output
436            $d =~ s/([^\\])-/$1\\-/g;
437            # replace single quotes
438            $d =~ s/\'/\\(aq/g;
439            # handle double quotes or periods first on the line
440            $d =~ s/^([\.\"])/\\&$1/;
441            # **bold**
442            $d =~ s/\*\*(\S.*?)\*\*/\\fB$1\\fP/g;
443            # *italics*
444            $d =~ s/\*(\S.*?)\*/\\fI$1\\fP/g;
445        }
446        else {
447            # **bold**
448            $d =~ s/\*\*(\S.*?)\*\*/$1/g;
449            # *italics*
450            $d =~ s/\*(\S.*?)\*/$1/g;
451        }
452        # trim trailing spaces
453        $d =~ s/[ \t]+\z//;
454        push @desc, "\n" if($blankline && !$header);
455        $blankline = 0;
456        push @desc, $d if($manpage);
457        my $qstr = $quote ? "q": "";
458        push @desc, "[".(1 + $level)."$qstr]$d" if(!$manpage);
459        $header = 0;
460
461    }
462    if($finalblank) {
463        print STDERR "$f:$line:1:ERROR: trailing blank line\n";
464        exit 3;
465    }
466    if($quote) {
467        # don't leave the quote "hanging"
468        push @desc, ".fi\n" if($manpage);
469    }
470    if($tablemode) {
471        # end of table
472        push @desc, ".RE\n.IP\n" if($manpage);
473    }
474    return @desc;
475}
476
477sub maybespace {
478    my ($string) = @_;
479
480    if(($string =~ /(.* )(.*)/) &&
481       (length($2) <= 20)) {
482        return $1;
483    }
484    if(($string =~ /(.*:)(.*)/) &&
485       (length($2) <= 20)) {
486        return $1;
487    }
488    return $string;
489}
490
491sub single {
492    my ($dir, $manpage, $f, $standalone)=@_;
493    my $fh;
494    open($fh, "<:crlf", "$dir/$f") ||
495        die "could not find $dir/$f";
496    my $short;
497    my $long;
498    my $tags;
499    my $added;
500    my $protocols;
501    my $arg;
502    my $mutexed;
503    my $requires;
504    my $category;
505    my @seealso;
506    my $copyright;
507    my $spdx;
508    my @examples; # there can be more than one
509    my $magic; # cmdline special option
510    my $line;
511    my $dline;
512    my $multi;
513    my $scope;
514    my $experimental;
515    my $start;
516    my $list; # identifies the list, 1 example, 2 see-also
517    while(<$fh>) {
518        $line++;
519        if(/^ *<!--/) {
520            next;
521        }
522        if(!$start) {
523            if(/^---/) {
524                $start = 1;
525            }
526            next;
527        }
528        if(/^Short: *(.)/i) {
529            $short=$1;
530        }
531        elsif(/^Long: *(.*)/i) {
532            $long=$1;
533        }
534        elsif(/^Added: *(.*)/i) {
535            $added=$1;
536        }
537        elsif(/^Tags: *(.*)/i) {
538            $tags=$1;
539        }
540        elsif(/^Arg: *(.*)/i) {
541            $arg=$1;
542        }
543        elsif(/^Magic: *(.*)/i) {
544            $magic=$1;
545        }
546        elsif(/^Mutexed: *(.*)/i) {
547            $mutexed=$1;
548        }
549        elsif(/^Protocols: *(.*)/i) {
550            $protocols=$1;
551        }
552        elsif(/^See-also: +(.+)/i) {
553            if($seealso) {
554                print STDERR "ERROR: duplicated See-also in $f\n";
555                return 1;
556            }
557            push @seealso, $1;
558        }
559        elsif(/^See-also:/i) {
560            $list=2;
561        }
562        elsif(/^  *- (.*)/i && ($list == 2)) {
563            push @seealso, $1;
564        }
565        elsif(/^Requires: *(.*)/i) {
566            $requires=$1;
567        }
568        elsif(/^Category: *(.*)/i) {
569            $category=$1;
570        }
571        elsif(/^Example: +(.+)/i) {
572            push @examples, $1;
573        }
574        elsif(/^Example:/i) {
575            # '1' is the example list
576            $list = 1;
577        }
578        elsif(/^  *- (.*)/i && ($list == 1)) {
579            push @examples, $1;
580        }
581        elsif(/^Multi: *(.*)/i) {
582            $multi=$1;
583        }
584        elsif(/^Scope: *(.*)/i) {
585            $scope=$1;
586        }
587        elsif(/^Experimental: yes/i) {
588            $experimental=1;
589        }
590        elsif(/^C: (.*)/i) {
591            $copyright=$1;
592        }
593        elsif(/^SPDX-License-Identifier: (.*)/i) {
594            $spdx=$1;
595        }
596        elsif(/^Help: *(.*)/i) {
597            ;
598        }
599        elsif(/^---/) {
600            $start++;
601            if(!$long) {
602                print STDERR "ERROR: no 'Long:' in $f\n";
603                return 1;
604            }
605            if(!$category) {
606                print STDERR "ERROR: no 'Category:' in $f\n";
607                return 2;
608            }
609            if(!$examples[0]) {
610                print STDERR "$f:$line:1:ERROR: no 'Example:' present\n";
611                return 2;
612            }
613            if(!$added) {
614                print STDERR "$f:$line:1:ERROR: no 'Added:' version present\n";
615                return 2;
616            }
617            if(!$seealso[0]) {
618                print STDERR "$f:$line:1:ERROR: no 'See-also:' field present\n";
619                return 2;
620            }
621            if(!$copyright) {
622                print STDERR "$f:$line:1:ERROR: no 'C:' field present\n";
623                return 2;
624            }
625            if(!$spdx) {
626                print STDERR "$f:$line:1:ERROR: no 'SPDX-License-Identifier:' field present\n";
627                return 2;
628            }
629            last;
630        }
631        else {
632            chomp;
633            print STDERR "$f:$line:1:WARN: unrecognized line in $f, ignoring:\n:'$_';"
634        }
635    }
636
637    if($start < 2) {
638        print STDERR "$f:1:1:ERROR: no proper meta-data header\n";
639        return 2;
640    }
641
642    my @desc = render($manpage, $fh, $f, $line);
643    close($fh);
644    if($tablemode) {
645        # end of table
646        push @desc, ".RE\n.IP\n";
647    }
648    my $opt;
649
650    if(defined($short) && $long) {
651        $opt = "-$short, --$long";
652    }
653    elsif($short && !$long) {
654        $opt = "-$short";
655    }
656    elsif($long && !$short) {
657        $opt = "--$long";
658    }
659
660    if($arg) {
661        $opt .= " $arg";
662    }
663
664    # quote "bare" minuses in opt
665    $opt =~ s/-/\\-/g if($manpage);
666    if($standalone) {
667        print ".TH curl 1 \"30 Nov 2016\" \"curl 7.52.0\" \"curl manual\"\n";
668        print ".SH OPTION\n";
669        print "curl $opt\n";
670    }
671    elsif($manpage) {
672        print ".IP \"$opt\"\n";
673    }
674    else {
675        lastline(1, $opt);
676    }
677    my @leading;
678    if($protocols) {
679        push @leading, protocols($manpage, $standalone, $protocols);
680    }
681
682    if($standalone) {
683        print ".SH DESCRIPTION\n";
684    }
685
686    if($experimental) {
687        push @leading, "**WARNING**: this option is experimental. Do not use in production.\n\n";
688    }
689
690    my $pre = $manpage ? "\n": "[1]";
691
692    if($scope) {
693        if($category !~ /global/) {
694            print STDERR "$f:$line:1:ERROR: global scope option does not have global category\n";
695            return 2;
696        }
697        if($scope eq "global") {
698            push @desc, "\n" if(!$manpage);
699            push @desc, "${pre}This option is global and does not need to be specified for each use of --next.\n";
700        }
701        else {
702            print STDERR "$f:$line:1:ERROR: unrecognized scope: '$scope'\n";
703            return 2;
704        }
705    }
706
707    my @extra;
708    if($multi eq "single") {
709        push @extra, "${pre}If --$long is provided several times, the last set ".
710            "value is used.\n";
711    }
712    elsif($multi eq "append") {
713        push @extra, "${pre}--$long can be used several times in a command line\n";
714    }
715    elsif($multi eq "boolean") {
716        my $rev = "no-$long";
717        # for options that start with "no-" the reverse is then without
718        # the no- prefix
719        if($long =~ /^no-/) {
720            $rev = $long;
721            $rev =~ s/^no-//;
722        }
723        my $dashes = $manpage ? "\\-\\-" : "--";
724        push @extra,
725            "${pre}Providing --$long multiple times has no extra effect.\n".
726            "Disable it again with $dashes$rev.\n";
727    }
728    elsif($multi eq "mutex") {
729        push @extra,
730            "${pre}Providing --$long multiple times has no extra effect.\n";
731    }
732    elsif($multi eq "custom") {
733        ; # left for the text to describe
734    }
735    elsif($multi eq "per-URL") {
736        push @extra,
737            "${pre}--$long is associated with a single URL. Use it once per URL ".
738            "when you use several URLs in a command line.\n";
739    }
740    else {
741        print STDERR "$f:$line:1:ERROR: unrecognized Multi: '$multi'\n";
742        return 2;
743    }
744
745    printdesc($manpage, 2, (@leading, @desc, @extra));
746    undef @desc;
747
748    my @foot;
749
750    my $mstr;
751    my $and = 0;
752    my $num = scalar(@seealso);
753    if($num > 2) {
754        # use commas up to this point
755        $and = $num - 1;
756    }
757    my $i = 0;
758    for my $k (@seealso) {
759        if(!$helplong{$k}) {
760            print STDERR "$f:$line:1:WARN: see-also a non-existing option: $k\n";
761        }
762        my $l = $manpage ? manpageify($k) : "--$k";
763        my $sep = " and";
764        if($and && ($i < $and)) {
765            $sep = ",";
766        }
767        $mstr .= sprintf "%s$l", $mstr?"$sep ":"";
768        $i++;
769    }
770
771    if($requires) {
772        my $l = $manpage ? manpageify($long) : "--$long";
773        push @foot, "$l requires that libcurl".
774            " is built to support $requires.\n";
775    }
776    if($mutexed) {
777        my @m=split(/ /, $mutexed);
778        my $mstr;
779        my $num = scalar(@m);
780        my $count;
781        for my $k (@m) {
782            if(!$helplong{$k}) {
783                print STDERR "WARN: $f mutexes a non-existing option: $k\n";
784            }
785            my $l = $manpage ? manpageify($k) : "--$k";
786            my $sep = ", ";
787            if($count == ($num -1)) {
788                $sep = " and ";
789            }
790            $mstr .= sprintf "%s$l", $mstr?$sep:"";
791            $count++;
792        }
793        push @foot, overrides($standalone,
794                              "This option is mutually exclusive with $mstr.\n");
795    }
796    if($examples[0]) {
797        my $s ="";
798        $s="s" if($examples[1]);
799        if($manpage) {
800            print "\nExample$s:\n";
801            print ".nf\n";
802            foreach my $e (@examples) {
803                $e =~ s!\$URL!https://example.com!g;
804                # convert single backslahes to doubles
805                $e =~ s/\\/\\\\/g;
806                print "curl $e\n";
807            }
808            print ".fi\n";
809        }
810        else {
811            my @ex;
812            push @ex, "[0q]Example$s:\n";
813            #
814            # long ASCII examples are wrapped. Preferably at the last space
815            # before the margin. Or at a colon. Otherwise it just cuts at the
816            # exact boundary.
817            #
818            foreach my $e (@examples) {
819                $e =~ s!\$URL!https://example.com!g;
820                my $maxwidth = 60; # plus the "    curl " 18 col prefix
821                if(length($e) > $maxwidth) {
822                    # a long example, shorten it
823                    my $p = substr($e, 0, $maxwidth);
824                    $p = maybespace($p);
825                    push @ex, "[0q] curl ".$p."\\";
826                    $e = substr($e, length($p));
827                    do {
828                        my $r = substr($e, 0, $maxwidth);
829                        if(length($e) > $maxwidth) {
830                            $r = maybespace($r);
831                        }
832                        my $slash ="";
833                        $e = substr($e, length($r));
834                        if(length($e) > 0) {
835                            $slash = "\\";
836                        }
837
838                        push @ex, "[0q]      $r$slash" if($r);
839                    } while(length($e));
840                }
841                else {
842                    push @ex, "[0q] curl $e\n";
843                }
844            }
845            printdesc($manpage, 2, @ex);
846        }
847    }
848    if($added) {
849        push @foot, added($standalone, $added);
850    }
851    push @foot, seealso($standalone, $mstr);
852
853    print "\n";
854    my $f = join("", @foot);
855    if($manpage) {
856        $f =~ s/ +\z//; # remove trailing space
857        print "$f\n";
858    }
859    else {
860        printdesc($manpage, 2, "[1]$f");
861    }
862    return 0;
863}
864
865sub getshortlong {
866    my ($dir, $f)=@_;
867    $f =~ s/^.*\///;
868    open(F, "<:crlf", "$dir/$f") ||
869        die "could not find $dir/$f";
870    my $short;
871    my $long;
872    my $help;
873    my $arg;
874    my $protocols;
875    my $category;
876    my $start = 0;
877    my $line = 0;
878    while(<F>) {
879        $line++;
880        if(!$start) {
881            if(/^---/) {
882                $start = 1;
883            }
884            next;
885        }
886        if(/^Short: (.)/i) {
887            $short=$1;
888        }
889        elsif(/^Long: (.*)/i) {
890            $long=$1;
891        }
892        elsif(/^Help: (.*)/i) {
893            $help=$1;
894            my $len = length($help);
895            if($len >= 49) {
896                printf STDERR "$f:$line:1:WARN: oversized help text: %d characters\n",
897                    $len;
898            }
899        }
900        elsif(/^Arg: (.*)/i) {
901            $arg=$1;
902        }
903        elsif(/^Protocols: (.*)/i) {
904            $protocols=$1;
905        }
906        elsif(/^Category: (.*)/i) {
907            $category=$1;
908        }
909        elsif(/^---/) {
910            last;
911        }
912    }
913    close(F);
914    if($short) {
915        $optshort{$short}=$long;
916    }
917    if($long) {
918        $optlong{$long}=$short;
919        $helplong{$long}=$help;
920        $arglong{$long}=$arg;
921        $protolong{$long}=$protocols;
922        $catlong{$long}=$category;
923    }
924}
925
926sub indexoptions {
927    my ($dir, @files) = @_;
928    foreach my $f (@files) {
929        getshortlong($dir, $f);
930    }
931}
932
933sub header {
934    my ($dir, $manpage, $f)=@_;
935    my $fh;
936    open($fh, "<:crlf", "$dir/$f") ||
937        die "could not find $dir/$f";
938    my @d = render($manpage, $fh, $f, 1);
939    close($fh);
940    printdesc($manpage, 0, @d);
941}
942
943
944sub sourcecategories {
945    my ($dir) = @_;
946    my %cats;
947    open(H, "<$dir/../../src/tool_help.h") ||
948        die "can't find the header file";
949    while(<H>) {
950        if(/^\#define CURLHELP_([A-Z0-9]*)/) {
951            $cats{lc($1)}++;
952        }
953    }
954    close(H);
955    return %cats;
956}
957
958sub listhelp {
959    my ($dir) = @_;
960    my %cats = sourcecategories($dir);
961
962    print <<HEAD
963/***************************************************************************
964 *                                  _   _ ____  _
965 *  Project                     ___| | | |  _ \\| |
966 *                             / __| | | | |_) | |
967 *                            | (__| |_| |  _ <| |___
968 *                             \\___|\\___/|_| \\_\\_____|
969 *
970 * Copyright (C) Daniel Stenberg, <daniel\@haxx.se>, et al.
971 *
972 * This software is licensed as described in the file COPYING, which
973 * you should have received as part of this distribution. The terms
974 * are also available at https://curl.se/docs/copyright.html.
975 *
976 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
977 * copies of the Software, and permit persons to whom the Software is
978 * furnished to do so, under the terms of the COPYING file.
979 *
980 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
981 * KIND, either express or implied.
982 *
983 * SPDX-License-Identifier: curl
984 *
985 ***************************************************************************/
986#include "tool_setup.h"
987#include "tool_help.h"
988
989/*
990 * DO NOT edit tool_listhelp.c manually.
991 * This source file is generated with the following command in an autotools
992 * build:
993 *
994 * "make listhelp"
995 */
996
997const struct helptxt helptext[] = {
998HEAD
999        ;
1000    foreach my $f (sort keys %helplong) {
1001        my $long = $f;
1002        my $short = $optlong{$long};
1003        my @categories = split ' ', $catlong{$long};
1004        my $bitmask = ' ';
1005        my $opt;
1006
1007        if(defined($short) && $long) {
1008            $opt = "-$short, --$long";
1009        }
1010        elsif($long && !$short) {
1011            $opt = "    --$long";
1012        }
1013        for my $i (0 .. $#categories) {
1014            if(!$cats{ $categories[$i] }) {
1015                printf STDERR "$f.md:ERROR: Unknown category '%s'\n",
1016                    $categories[$i];
1017                exit 3;
1018            }
1019
1020            $bitmask .= 'CURLHELP_' . uc $categories[$i];
1021            # If not last element, append |
1022            if($i < $#categories) {
1023                $bitmask .= ' | ';
1024            }
1025        }
1026        $bitmask =~ s/(?=.{76}).{1,76}\|/$&\n  /g;
1027        my $arg = $arglong{$long};
1028        if($arg) {
1029            $opt .= " $arg";
1030        }
1031        my $desc = $helplong{$f};
1032        $desc =~ s/\"/\\\"/g; # escape double quotes
1033
1034        my $line = sprintf "  {\"%s\",\n   \"%s\",\n  %s},\n", $opt, $desc, $bitmask;
1035
1036        if(length($opt) > 78) {
1037            print STDERR "WARN: the --$long name is too long\n";
1038        }
1039        elsif(length($desc) > 78) {
1040            print STDERR "WARN: the --$long description is too long\n";
1041        }
1042        print $line;
1043    }
1044    print <<FOOT
1045  { NULL, NULL, 0 }
1046};
1047FOOT
1048        ;
1049}
1050
1051sub listcats {
1052    my %allcats;
1053    foreach my $f (sort keys %helplong) {
1054        my @categories = split ' ', $catlong{$f};
1055        foreach (@categories) {
1056            $allcats{$_} = undef;
1057        }
1058    }
1059    my @categories;
1060    foreach my $key (keys %allcats) {
1061        push @categories, $key;
1062    }
1063    @categories = sort @categories;
1064    for my $i (0..$#categories) {
1065        printf("#define CURLHELP_%-10s (%s)\n",
1066               uc($categories[$i]), "1u << ${i}u");
1067    }
1068}
1069
1070sub listglobals {
1071    my ($dir, @files) = @_;
1072    my @globalopts;
1073
1074    # Find all global options and output them
1075    foreach my $f (sort @files) {
1076        open(F, "<:crlf", "$dir/$f") ||
1077            die "could not read $dir/$f";
1078        my $long;
1079        my $start = 0;
1080        while(<F>) {
1081            if(/^---/) {
1082                if(!$start) {
1083                    $start = 1;
1084                    next;
1085                }
1086                else {
1087                    last;
1088                }
1089            }
1090            if(/^Long: *(.*)/i) {
1091                $long=$1;
1092            }
1093            elsif(/^Scope: global/i) {
1094                push @globalopts, $long;
1095                last;
1096            }
1097        }
1098        close(F);
1099    }
1100    return $ret if($ret);
1101    for my $e (0 .. $#globalopts) {
1102        $globals .= sprintf "%s--%s",  $e?($globalopts[$e+1] ? ", " : " and "):"",
1103            $globalopts[$e],;
1104    }
1105}
1106
1107sub noext {
1108    my $in = $_[0];
1109    $in =~ s/\.md//;
1110    return $in;
1111}
1112
1113sub sortnames {
1114    return noext($a) cmp noext($b);
1115}
1116
1117sub mainpage {
1118    my ($dir, $manpage, @files) = @_;
1119    # $manpage is 1 for nroff, 0 for ASCII
1120    my $ret;
1121    my $fh;
1122    open($fh, "<:crlf", "$dir/mainpage.idx") ||
1123        die "no $dir/mainpage.idx file";
1124
1125    print <<HEADER
1126.\\" **************************************************************************
1127.\\" *                                  _   _ ____  _
1128.\\" *  Project                     ___| | | |  _ \\| |
1129.\\" *                             / __| | | | |_) | |
1130.\\" *                            | (__| |_| |  _ <| |___
1131.\\" *                             \\___|\\___/|_| \\_\\_____|
1132.\\" *
1133.\\" * Copyright (C) Daniel Stenberg, <daniel\@haxx.se>, et al.
1134.\\" *
1135.\\" * This software is licensed as described in the file COPYING, which
1136.\\" * you should have received as part of this distribution. The terms
1137.\\" * are also available at https://curl.se/docs/copyright.html.
1138.\\" *
1139.\\" * You may opt to use, copy, modify, merge, publish, distribute and/or sell
1140.\\" * copies of the Software, and permit persons to whom the Software is
1141.\\" * furnished to do so, under the terms of the COPYING file.
1142.\\" *
1143.\\" * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
1144.\\" * KIND, either express or implied.
1145.\\" *
1146.\\" * SPDX-License-Identifier: curl
1147.\\" *
1148.\\" **************************************************************************
1149.\\"
1150.\\" DO NOT EDIT. Generated by the curl project managen manpage generator.
1151.\\"
1152.TH curl 1 "$date" "curl $version" "curl Manual"
1153HEADER
1154        if ($manpage);
1155
1156    while(<$fh>) {
1157        my $f = $_;
1158        chomp $f;
1159        if($f =~ /^#/) {
1160            # standard comment
1161            next;
1162        }
1163        if(/^%options/) {
1164            # output docs for all options
1165            foreach my $f (sort sortnames @files) {
1166                $ret += single($dir, $manpage, $f, 0);
1167            }
1168        }
1169        else {
1170            # render the file
1171            header($dir, $manpage, $f);
1172        }
1173    }
1174    close($fh);
1175    exit $ret if($ret);
1176}
1177
1178sub showonly {
1179    my ($f) = @_;
1180    if(single($f, 1)) {
1181        print STDERR "$f: failed\n";
1182    }
1183}
1184
1185sub showprotocols {
1186    my %prots;
1187    foreach my $f (keys %optlong) {
1188        my @p = split(/ /, $protolong{$f});
1189        for my $p (@p) {
1190            $prots{$p}++;
1191        }
1192    }
1193    for(sort keys %prots) {
1194        printf "$_ (%d options)\n", $prots{$_};
1195    }
1196}
1197
1198sub getargs {
1199    my ($dir, $f, @s) = @_;
1200    if($f eq "mainpage") {
1201        listglobals($dir, @s);
1202        mainpage($dir, 1, @s);
1203        return;
1204    }
1205    elsif($f eq "ascii") {
1206        listglobals($dir, @s);
1207        mainpage($dir, 0, @s);
1208        return;
1209    }
1210    elsif($f eq "listhelp") {
1211        listhelp($dir);
1212        return;
1213    }
1214    elsif($f eq "single") {
1215        showonly($s[0]);
1216        return;
1217    }
1218    elsif($f eq "protos") {
1219        showprotocols();
1220        return;
1221    }
1222    elsif($f eq "listcats") {
1223        listcats();
1224        return;
1225    }
1226
1227    print "Usage: managen ".
1228        "[-d dir] <mainpage/ascii/listhelp/single FILE/protos/listcats> [files]\n";
1229}
1230
1231#------------------------------------------------------------------------
1232
1233my $dir = ".";
1234my $include = "../../include";
1235my $cmd = shift @ARGV;
1236
1237 check:
1238if($cmd eq "-d") {
1239    # specifies source directory
1240    $dir = shift @ARGV;
1241    $cmd = shift @ARGV;
1242    goto check;
1243}
1244elsif($cmd eq "-I") {
1245    # include path root
1246    $include = shift @ARGV;
1247    $cmd = shift @ARGV;
1248    goto check;
1249}
1250elsif($cmd eq "-c") {
1251    # Column width
1252    $colwidth = 0 + shift @ARGV;
1253    $cmd = shift @ARGV;
1254    goto check;
1255}
1256
1257my @files = @ARGV; # the rest are the files
1258
1259# can be overriden for releases
1260if($ENV{'CURL_MAKETGZ_VERSION'}) {
1261    $version = $ENV{'CURL_MAKETGZ_VERSION'};
1262}
1263else {
1264    open(INC, "<$include/curl/curlver.h");
1265    while(<INC>) {
1266        if($_ =~ /^#define LIBCURL_VERSION \"([0-9.]*)/) {
1267            $version = $1;
1268            last;
1269        }
1270    }
1271    close(INC);
1272}
1273
1274# learn all existing options
1275indexoptions($dir, @files);
1276
1277getargs($dir, $cmd, @files);
1278
1279exit $error;
1280