#!/usr/bin/env perl
#***************************************************************************
#                                  _   _ ____  _
#  Project                     ___| | | |  _ \| |
#                             / __| | | | |_) | |
#                            | (__| |_| |  _ <| |___
#                             \___|\___/|_| \_\_____|
#
# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at https://curl.se/docs/copyright.html.
#
# You may opt to use, copy, modify, merge, publish, distribute and/or sell
# copies of the Software, and permit persons to whom the Software is
# furnished to do so, under the terms of the COPYING file.
#
# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
# KIND, either express or implied.
#
# SPDX-License-Identifier: curl
#
###########################################################################

=begin comment

This script generates the man page.

Example: managen <command> [files] > curl.1

Dev notes:

We open *input* files in :crlf translation (a no-op on many platforms) in
case we have CRLF line endings in Windows but a perl that defaults to LF.
Unfortunately it seems some perls like msysgit cannot handle a global input-only
:crlf so it has to be specified on each file open for text input.

=end comment
=cut

use strict;
use warnings;

my %optshort;
my %optlong;
my %helplong;
my %arglong;
my %redirlong;
my %protolong;
my %catlong;

use POSIX qw(strftime);
my @ts;
if(defined($ENV{SOURCE_DATE_EPOCH})) {
    @ts = gmtime($ENV{SOURCE_DATE_EPOCH});
} else {
    @ts = localtime;
}
my $date = strftime "%Y-%m-%d", @ts;
my $year = strftime "%Y", @ts;
my $version = "unknown";
my $globals;
my $error = 0;
my $indent = 4;

# get the long name version, return the man page string
sub manpageify {
    my ($k, $manpage) = @_;
    my $trail = '';
    # the matching pattern might include a trailing dot that cannot be part of
    # the option name
    if($k =~ s/\.$//) {
        # cut off trailing dot
        $trail = ".";
    }
    if($manpage) {
        my $klong = $k;
        # quote "bare" minuses in the long name
        $klong =~ s/-/\\-/g;

        # only long
        return "\\fI\\-\\-$klong\\fP$trail";
    }
    return "--$k$trail";
}

my $colwidth = 79; # max number of columns

sub prefixline {
    my ($num) = @_;
    print "\t" x ($num / 8);
    print ' ' x ($num % 8);
}

sub justline {
    my ($lvl, @line) = @_;
    my $w = -1;
    my $spaces = -1;
    my $width = $colwidth - ($lvl * $indent);
    for(@line) {
        $w += length($_);
        $w++;
        $spaces++;
    }
    my $inject = $width - $w;
    my $ratio = 0; # stay at zero if no spaces at all
    if($spaces) {
        $ratio = $inject / $spaces;
    }
    my $spare = 0;
    prefixline($lvl * $indent);
    my $prev;
    for(@line) {
        while($spare >= 0.90) {
            print " ";
            $spare--;
        }
        printf "%s%s", $prev ? " " : "", $_;
        $prev = 1;
        $spare += $ratio;
    }
    print "\n";
}

sub lastline {
    my ($lvl, @line) = @_;
    my $l = 0;
    $line[0] =~ s/^( +)//;
    $l = length($1) if($1);
    prefixline($lvl * $indent + $l);
    my $prev = 0;
    for(@line) {
        printf "%s%s", $prev ? " " : "", $_;
        $prev = 1;
    }
    print "\n";
}

sub outputpara {
    my ($lvl, $f) = @_;
    $f =~ s/\n/ /g;

    my $w = 0;
    my @words = split(/  */, $f);
    my $width = $colwidth - ($lvl * $indent);

    my @line;
    for my $e (@words) {
        my $l = length($e);
        my $spaces = scalar(@line);
        if(($w + $l + $spaces) >= $width) {
            justline($lvl, @line);
            undef @line;
            $w = 0;
        }

        push @line, $e;
        $w += $l; # new width
    }
    if($w) {
        lastline($lvl, @line);
        print "\n";
    }
}

sub printdesc {
    my ($manpage, $baselvl, @desc) = @_;

    if($manpage) {
        for my $d (@desc) {
            print $d;
        }
    }
    else {
        my $p = -1;
        my $pnum;
        my $para = '';
        for my $l (@desc) {
            my $lvl = 0;
            my $lvlnum;
            if($l !~ /^[\n\r]+/) {
                # get the indent level off the string
                $l =~ s/^\[([0-9q]*)\]//;
                $lvl = $1;
            }
            if(($p =~ /q/) && ($lvl !~ /q/)) {
                # the previous was quoted, this is not
                print "\n";
            }
            if($lvl ne $p) {
                $pnum = $p;
                $pnum =~ s/q$//;
                outputpara($baselvl + $pnum, $para);
                $para = "";
            }
            if($lvl =~ /q/) {
                # quoted, do not right-justify
                chomp $l;
                $lvlnum = $lvl;
                $lvlnum =~ s/q$//;
                lastline($baselvl + $lvlnum + 1, $l);
                my $w = ($baselvl + $lvlnum + 1) * $indent + length($l);
                if($w > $colwidth) {
                    print STDERR "ERROR: $w columns is too long\n";
                    print STDERR "$l\n";
                    $error++;
                }
            }
            else {
                $para .= $l;
            }

            $p = $lvl;
        }
        $pnum = $p;
        $pnum =~ s/q$//;
        outputpara($baselvl + $pnum, $para);
    }
}

sub seealso {
    my($standalone, $data) = @_;
    if($standalone) {
        return sprintf
            ".SH \"SEE ALSO\"\n$data\n";
    }
    else {
        return "See also $data. ";
    }
}

sub overrides {
    my ($standalone, $data) = @_;
    if($standalone) {
        return ".SH \"OVERRIDES\"\n$data\n";
    }
    else {
        return $data;
    }
}

my %protexists = (
    'DNS' => 1,
    'FILE' => 1,
    'FTP' => 1,
    'FTPS' => 1,
    'GSS/kerberos' => 1,
    'HTTP' => 1,
    'HTTPS' => 1,
    'IMAP' => 1,
    'IPFS' => 1,
    'LDAP' => 1,
    'MQTT' => 1,
    'POP3' => 1,
    'SCP' => 1,
    'SFTP' => 1,
    'SMTP' => 1,
    'SSL' => 2, # deprecated
    'TELNET' => 1,
    'TFTP' => 1,
    'TLS' => 1,
    );

sub protocols {
    my ($f, $line, $manpage, $standalone, $data) = @_;
    my @e = split(/ +/, $data);
    for my $pr (@e) {
        if(!$protexists{$pr}) {

            print STDERR "$f:$line:1:ERROR: unrecognized protocol: $pr\n";
            exit 2;
        }
    }
    if($standalone) {
        return ".SH \"PROTOCOLS\"\n$data\n";
    }
    else {
        return "($data) " if($manpage);
        return "[1]($data) " if(!$manpage);
    }
}

sub too_old {
    my ($version) = @_;
    my $a = 999999;
    if($version =~ /^(\d+)\.(\d+)\.(\d+)/) {
        $a = $1 * 1000 + $2 * 10 + $3;
    }
    elsif($version =~ /^(\d+)\.(\d+)/) {
        $a = $1 * 1000 + $2 * 10;
    }
    if($a < 7660) {
        # we consider everything before 7.66.0 to be too old to mention
        # specific changes for
        return 1;
    }
    return 0;
}

sub added {
    my ($standalone, $data) = @_;
    if(too_old($data)) {
        # do not mention ancient additions
        return "";
    }
    if($standalone) {
        return ".SH \"ADDED\"\nAdded in curl version $data\n";
    }
    else {
        return "Added in $data. ";
    }
}

sub render {
    my ($manpage, $fh, $f, $line) = @_;
    my @desc;
    my $tablemode = 0;
    my $header = 0;
    # if $top is TRUE, it means a top-level page and not a command line option
    my $top = ($line == 1);
    my $quote = 0;
    my $level = 0;
    my $finalblank;
    my $blankline = 0;
    my $start = 0;

    while(<$fh>) {
        my $d = $_;
        $line++;
        $finalblank = ($d eq "\n");
        if($d =~ /^\.(SH|BR|IP|B)/) {
            print STDERR "$f:$line:1:ERROR: nroff instruction in input: \".$1\"\n";
            return 4;
        }
        if(/^ *<!--/) {
            # skip comments
            next;
        }
        if((!$start) && ($_ =~ /^[\r\n]*\z/)) {
            # skip leading blank lines
            next;
        }

        $start = 1;

        if(/^[ \t]*\n/) {
            # count and ignore blank lines
            $blankline++;
            next;
        }
        elsif($d =~ /^    (.*)/) {
            my $word = $1;
            if(!$quote && $manpage) {
                push @desc, "\n" if($blankline);
                push @desc, ".nf\n";
                $blankline = 0;
            }
            $quote = 1;
            $d = "$word\n";
        }
        elsif($d =~ /^(```|~~~)/) {
            if(!$quote) {
                if($manpage) {
                    push @desc, "\n" if($blankline);
                    push @desc, ".nf\n";
                    $blankline = 0;
                }
                $quote = 2;
            }
            else {
                # end of quote
                push @desc, ".fi\n" if($manpage);
                $quote = 0;
            }
            next;
        }
        elsif($quote == 1) {
            # end of quote
            push @desc, ".fi\n" if($manpage);
            $quote = 0;
        }

        if(/^# (.*)/) {
            $header = 1;
            if($top != 1) {
                # ignored for command line options
                $blankline++;
                next;
            }
            push @desc, ".SH $1\n" if($manpage);
            push @desc, "[0]$1\n" if(!$manpage);
            next;
        }
        elsif(/^###/) {
            print STDERR "$f:$line:1:ERROR: ### header is not supported\n";
            exit 3;
        }
        elsif(/^## (.*)/) {
            my $word = $1;
            my $nontick = $word;
            # if there are enclosing quotes, remove them first
            $word =~ s/[\"\'](.*)[\"\']\z/$1/;

            # remove backticks from headers
            $word =~ s/\`//g;

            # remove backticked sections
            $nontick =~ s/\`[^\`]*\`//g;
            if($nontick =~ /[^\\][\<\>]/) {
                print STDERR "$f:$line:1:WARN: un-escaped < or > used: $nontick\n";
            }

            # if there is a space, it needs quotes for man page
            if(($word =~ / /) && $manpage) {
                $word = "\"$word\"";
            }
            $level = 1;
            if($top == 1) {
                push @desc, ".IP $word\n" if($manpage);
                push @desc, "\n" if(!$manpage);
                push @desc, "[1]$word\n" if(!$manpage);
            }
            else {
                if(!$tablemode) {
                    push @desc, ".RS\n" if($manpage);
                    $tablemode = 1;
                }
                push @desc, ".IP $word\n" if($manpage);
                push @desc, "\n" if(!$manpage);
                push @desc, "[1]$word\n" if(!$manpage);
            }
            $header = 1;
            next;
        }
        elsif(/^##/) {
            if($top == 1) {
                print STDERR "$f:$line:1:ERROR: ## empty header top-level mode\n";
                exit 3;
            }
            if($tablemode) {
                # end of table
                push @desc, ".RE\n.IP\n" if($manpage);
                push @desc, "\n" if(!$manpage);
                $tablemode = 0;
                $level = 0;
            }
            $header = 1;
            next;
        }
        elsif(/^\.(IP|RS|RE)/) {
            my ($cmd) = ($1);
            print STDERR "$f:$line:1:ERROR: $cmd detected, use ##-style\n";
            return 3;
        }

        $d =~ s/`%DATE`/$date/g;
        $d =~ s/`%VERSION`/$version/g;
        $d =~ s/`%GLOBALS`/$globals/g;

        if(!$quote) {
            if($d =~ /^(.*)  /) {
                printf STDERR "$f:$line:%d:ERROR: 2 spaces detected\n",
                    length($1);
                return 3;
            }
            my $back = $d;

            # remove all backticked pieces
            $back =~ s/\`(.*?)\`//g;

            if($back =~ /[^\\][\<\>]/) {
                print STDERR "$f:$line:1:WARN: un-escaped < or > used: $back\n";
                return 3;
            }
        }

        # convert backticks to double quotes
        $d =~ s/\`/\"/g;

        if($d =~ /\(added in(.*)/i) {
            if(length($1) < 2) {
                print STDERR "$f:$line:1:ERROR: broken up added-in line:\n";
                print STDERR "$f:$line:1:ERROR: $d";
                return 3;
            }
        }
      again:
        if($d =~ /\(Added in ([0-9.]+)\)/i) {
            my $ver = $1;
            if(too_old($ver)) {
                $d =~ s/ *\(Added in $ver\)//gi;
                goto again;
            }
        }

        # convert backslash-'<' or '> to the second character
        $d =~ s/\\([><])/$1/g;
        # convert single backslash to double-backslash
        $d =~ s/\\/\\\\/g if($manpage);

        if($manpage) {
            if(!$quote && $d =~ /--/) {
                $d =~ s/--([a-z0-9.-]+)/manpageify($1, 1)/ge;
            }

            # quote minuses in the output
            $d =~ s/([^\\])-/$1\\-/g;
            # replace single quotes
            $d =~ s/\'/\\(aq/g;
            # handle double quotes or periods first on the line
            $d =~ s/^([\.\"])/\\&$1/;
            # **bold**
            $d =~ s/\*\*(\S.*?)\*\*/\\fB$1\\fP/g;
            # *italics*
            $d =~ s/\*(\S.*?)\*/\\fI$1\\fP/g;
        }
        else {
            # **bold**
            $d =~ s/\*\*(\S.*?)\*\*/$1/g;
            # *italics*
            $d =~ s/\*(\S.*?)\*/$1/g;
        }
        # trim trailing spaces
        $d =~ s/[ \t]+\z//;
        push @desc, "\n" if($blankline && !$header);
        $blankline = 0;
        push @desc, $d if($manpage);
        my $qstr = $quote ? "q" : "";
        push @desc, "[".(1 + $level)."$qstr]$d" if(!$manpage);
        $header = 0;
    }
    if($finalblank) {
        print STDERR "$f:$line:1:ERROR: trailing blank line\n";
        exit 3;
    }
    if($quote) {
        # do not leave the quote "hanging"
        push @desc, ".fi\n" if($manpage);
    }
    if($tablemode) {
        # end of table
        push @desc, ".RE\n.IP\n" if($manpage);
    }
    return @desc;
}

sub maybespace {
    my ($string) = @_;

    if(($string =~ /(.* )(.*)/) &&
       (length($2) <= 20)) {
        return $1;
    }
    if(($string =~ /(.*:)(.*)/) &&
       (length($2) <= 20)) {
        return $1;
    }
    return $string;
}

sub single {
    my ($dir, $manpage, $f, $standalone) = @_;
    my $fh;
    open($fh, "<:crlf", "$dir/$f") or
        die "could not find $dir/$f";
    my $short;
    my $long;
    my $tags;
    my $added;
    my $protocols;
    my $arg;
    my $mutexed;
    my $requires;
    my $category;
    my @seealso;
    my $copyright;
    my $spdx;
    my @examples; # there can be more than one
    my $magic; # cmdline special option
    my $line;
    my $dline;
    my $multi = "";
    my $scope;
    my $experimental;
    my $start = 0;
    my $list; # identifies the list, 1 example, 2 see-also
    while(<$fh>) {
        $line++;
        if(/^ *<!--/) {
            next;
        }
        if(!$start) {
            if(/^---/) {
                $start = 1;
            }
            next;
        }
        if(/^Short: *(.)/i) {
            $short = $1;
        }
        elsif(/^Long: *(.*)/i) {
            $long = $1;
        }
        elsif(/^Added: *(.*)/i) {
            $added = $1;
        }
        elsif(/^Tags: *(.*)/i) {
            $tags = $1;
        }
        elsif(/^Arg: *(.*)/i) {
            $arg = $1;
        }
        elsif(/^Magic: *(.*)/i) {
            $magic = $1;
        }
        elsif(/^Mutexed: *(.*)/i) {
            $mutexed = $1;
        }
        elsif(/^Protocols: *(.*)/i) {
            $protocols = $1;
        }
        elsif(/^See-also: +(.+)/i) {
            if(@seealso) {
                print STDERR "ERROR: duplicated See-also in $f\n";
                return 1;
            }
            push @seealso, $1;
        }
        elsif(/^See-also:/i) {
            $list = 2;
        }
        elsif(/^  *- (.*)/i && ($list == 2)) {
            push @seealso, $1;
        }
        elsif(/^Requires: *(.*)/i) {
            $requires = $1;
        }
        elsif(/^Category: *(.*)/i) {
            $category = $1;
        }
        elsif(/^Example: +(.+)/i) {
            push @examples, $1;
        }
        elsif(/^Example:/i) {
            # '1' is the example list
            $list = 1;
        }
        elsif(/^  *- (.*)/i && ($list == 1)) {
            push @examples, $1;
        }
        elsif(/^Multi: *(.*)/i) {
            $multi = $1;
        }
        elsif(/^Scope: *(.*)/i) {
            $scope = $1;
        }
        elsif(/^Experimental: yes/i) {
            $experimental = 1;
        }
        # REUSE-IgnoreStart
        elsif(/^C: (.*)/i) {
            $copyright = $1;
        }
        elsif(/^SPDX-License-Identifier: (.*)/i) {
            $spdx = $1;
        }
        # REUSE-IgnoreEnd
        elsif(/^Help: *(.*)/i) {
            ;
        }
        elsif(/^---/) {
            $start++;
            if(!$long) {
                print STDERR "ERROR: no 'Long:' in $f\n";
                return 1;
            }
            if(!$category) {
                print STDERR "ERROR: no 'Category:' in $f\n";
                return 2;
            }
            if(!$examples[0]) {
                print STDERR "$f:$line:1:ERROR: no 'Example:' present\n";
                return 2;
            }
            if(!$added) {
                print STDERR "$f:$line:1:ERROR: no 'Added:' version present\n";
                return 2;
            }
            if(!$seealso[0]) {
                print STDERR "$f:$line:1:ERROR: no 'See-also:' field present\n";
                return 2;
            }
            if(!$copyright) {
                print STDERR "$f:$line:1:ERROR: no 'C:' field present\n";
                return 2;
            }
            if(!$spdx) {
                # REUSE-IgnoreStart
                print STDERR "$f:$line:1:ERROR: no 'SPDX-License-Identifier:' field present\n";
                # REUSE-IgnoreEnd
                return 2;
            }
            last;
        }
        else {
            chomp;
            print STDERR "$f:$line:1:WARN: unrecognized line in $f, ignoring: '$_';\n";
        }
    }

    if($start < 2) {
        print STDERR "$f:1:1:ERROR: no proper meta-data header\n";
        return 2;
    }

    my @desc = render($manpage, $fh, $f, $line);
    close($fh);
    my $opt;

    if(defined($short) && $long) {
        $opt = "-$short, --$long";
    }
    elsif($short && !$long) {
        $opt = "-$short";
    }
    elsif($long && !$short) {
        $opt = "--$long";
    }

    if($arg) {
        $opt .= " $arg";
    }

    # quote "bare" minuses in opt
    $opt =~ s/-/\\-/g if($manpage);
    if($standalone) {
        print ".TH curl 1 \"30 Nov 2016\" \"curl 7.52.0\" \"curl manual\"\n";
        print ".SH OPTION\n";
        print "curl $opt\n";
    }
    elsif($manpage) {
        print ".IP \"$opt\"\n";
    }
    else {
        lastline(1, $opt);
    }
    my @leading;
    if($protocols) {
        push @leading, protocols($f, $line, $manpage, $standalone, $protocols);
    }

    if($standalone) {
        print ".SH DESCRIPTION\n";
    }

    if($experimental) {
        my $pref = $manpage ? "" : "[1]";
        push @leading, "$pref**WARNING**: this option is experimental. Do not use in production.\n\n";
    }

    my $pre = $manpage ? "\n" : "[1]";

    if($scope) {
        if($category !~ /global/) {
            print STDERR "$f:$line:1:ERROR: global scope option does not have global category\n";
            return 2;
        }
        if($scope eq "global") {
            push @desc, "\n" if(!$manpage);
            push @desc,
                sprintf("${pre}This option is global and does not need to be specified for each use of %s.\n",
                        manpageify("next", $manpage));
        }
        else {
            print STDERR "$f:$line:1:ERROR: unrecognized scope: '$scope'\n";
            return 2;
        }
    }

    my @extra;
    if($multi eq "single") {
        push @extra,
            sprintf("${pre}If %s is provided several times, the last set ".
                    "value is used.\n",
                    manpageify($long, $manpage));
    }
    elsif($multi eq "append") {
        push @extra,
            sprintf("${pre}%s can be used several times in a command line.\n",
                    manpageify($long, $manpage));
    }
    elsif($multi eq "boolean") {
        my $rev = "no-$long";
        # for options that start with "no-" the reverse is then without
        # the no- prefix
        if($long =~ /^no-/) {
            $rev = $long;
            $rev =~ s/^no-//;
        }
        my $dashes = $manpage ? "\\-\\-" : "--";
        push @extra,
            sprintf("${pre}Providing %s multiple times has no extra effect.\n".
                    "Disable it again with $dashes$rev.\n",
                    manpageify($long, $manpage));
    }
    elsif($multi eq "mutex") {
        push @extra,
            sprintf("${pre}Providing %s multiple times has no extra effect.\n",
                    manpageify($long, $manpage));
    }
    elsif($multi eq "custom") {
        ; # left for the text to describe
    }
    elsif($multi eq "per-URL") {
        push @extra,
            sprintf("${pre}%s is associated with a single URL. Use it once per URL ".
                    "when you use several URLs in a command line.\n",
                    manpageify($long, $manpage));
    }
    elsif($multi) {
        print STDERR "$f:$line:1:ERROR: unrecognized Multi: '$multi'\n";
        return 2;
    }

    printdesc($manpage, 2, (@leading, @desc, @extra));
    undef @desc;

    my @foot;

    my $mstr;
    my $and = 0;
    my $num = scalar(@seealso);
    if($num > 2) {
        # use commas up to this point
        $and = $num - 1;
    }
    my $i = 0;
    for my $k (@seealso) {
        if(!$helplong{$k}) {
            print STDERR "$f:$line:1:WARN: see-also a non-existing option: $k\n";
        }
        my $l = manpageify($k, $manpage);
        my $sep = " and";
        if($and && ($i < $and)) {
            $sep = ",";
        }
        $mstr .= sprintf "%s$l", $mstr ? "$sep " : "";
        $i++;
    }

    if($requires) {
        my $l = manpageify($long, $manpage);
        push @foot, "For $l to work, it requires that the underlying libcurl".
            " is built to support $requires.\n";
    }
    if($mutexed) {
        my @m = split(/ /, $mutexed);
        my $mstr;
        my $num = scalar(@m);
        my $count = 0;
        for my $k (@m) {
            if(!$helplong{$k}) {
                print STDERR "WARN: $f mutexes a non-existing option: $k\n";
            }
            my $l = manpageify($k, $manpage);
            my $sep = ", ";
            if($count == ($num -1)) {
                $sep = " and ";
            }
            $mstr .= sprintf "%s$l", $mstr ? $sep : "";
            $count++;
        }
        push @foot, overrides($standalone,
                              "This option is mutually exclusive with $mstr.\n");
    }
    if($examples[0]) {
        my $s = "";
        $s = "s" if($examples[1]);
        foreach my $e (@examples) {
            my $check = $e;
            # verify the used options
            for my $e (split(/ /, $check)) {
                if($e =~ /^-([^- ])/) {
                    my $opt = $1;
                    if(!$optshort{$opt}) {
                        print STDERR "$f:$line:1:ERROR: unknown option in ".
                            "example: -$opt\n";
                        return 2;
                    }
                }
                elsif($e =~ /^--([^ =]*)/) {
                    my $opt = $1;
                    $opt =~ s/^expand-//g;
                    if(!$helplong{$opt}) {
                        print STDERR "$f:$line:1:ERROR: unknown option in ".
                            "example: '--$opt'\n";
                        return 2;
                    }
                }
            }
        }
        if($manpage) {
            print "\nExample$s:\n";
            print ".nf\n";
            foreach my $e (@examples) {
                $e =~ s!\$URL!https://example.com!g;
                # convert single backslashes to doubles
                $e =~ s/\\/\\\\/g;
                print "curl $e\n";
            }
            print ".fi\n";
        }
        else {
            my @ex;
            push @ex, "[0q]Example$s:\n";
            #
            # long ASCII examples are wrapped. Preferably at the last space
            # before the margin. Or at a colon. Otherwise it cuts at the
            # exact boundary.
            #
            foreach my $e (@examples) {
                $e =~ s!\$URL!https://example.com!g;
                my $maxwidth = 60; # plus the "    curl " 18 col prefix
                if(length($e) > $maxwidth) {
                    # a long example, shorten it
                    my $p = substr($e, 0, $maxwidth);
                    $p = maybespace($p);
                    push @ex, "[0q] curl ".$p."\\";
                    $e = substr($e, length($p));
                    do {
                        my $r = substr($e, 0, $maxwidth);
                        if(length($e) > $maxwidth) {
                            $r = maybespace($r);
                        }
                        my $slash = "";
                        $e = substr($e, length($r));
                        if(length($e) > 0) {
                            $slash = "\\";
                        }

                        push @ex, "[0q]      $r$slash" if($r);
                    } while(length($e));
                }
                else {
                    push @ex, "[0q] curl $e\n";
                }
            }
            printdesc($manpage, 2, @ex);
        }
    }
    if($added) {
        push @foot, added($standalone, $added);
    }
    push @foot, seealso($standalone, $mstr);

    print "\n";
    my $ft = join("", @foot);
    if($manpage) {
        $ft =~ s/ +\z//; # remove trailing space
        print "$ft\n";
    }
    else {
        printdesc($manpage, 2, "[1]$ft");
    }
    return 0;
}

sub getshortlong {
    my ($dir, $f) = @_;
    $f =~ s/^.*\///;
    open(F, "<:crlf", "$dir/$f") or
        die "could not find $dir/$f";
    my $short;
    my $long;
    my $help;
    my $arg;
    my $protocols;
    my $category;
    my $start = 0;
    my $line = 0;
    while(<F>) {
        $line++;
        if(!$start) {
            if(/^---/) {
                $start = 1;
            }
            next;
        }
        if(/^Short: (.)/i) {
            $short = $1;
        }
        elsif(/^Long: (.*)/i) {
            $long = $1;
        }
        elsif(/^Help: (.*)/i) {
            $help = $1;
            my $len = length($help);
            if($len >= 49) {
                printf STDERR "$f:$line:1:WARN: oversized help text: %d characters\n",
                    $len;
            }
        }
        elsif(/^Arg: (.*)/i) {
            $arg = $1;
        }
        elsif(/^Protocols: (.*)/i) {
            $protocols = $1;
        }
        elsif(/^Category: (.*)/i) {
            $category = $1;
        }
        elsif(/^---/) {
            last;
        }
    }
    close(F);
    if($short) {
        $optshort{$short} = $long;
    }
    if($long) {
        $optlong{$long} = $short;
        $helplong{$long} = $help;
        $arglong{$long} = $arg;
        $protolong{$long} = $protocols;
        $catlong{$long} = $category;
    }
}

sub indexoptions {
    my ($dir, @files) = @_;
    foreach my $file (@files) {
        getshortlong($dir, $file);
    }
}

sub header {
    my ($dir, $manpage, $f) = @_;
    my $fh;
    open($fh, "<:crlf", "$dir/$f") or
        die "could not find $dir/$f";
    my @d = render($manpage, $fh, $f, 1);
    close($fh);
    printdesc($manpage, 0, @d);
}

sub sourcecategories {
    my ($dir) = @_;
    my %cats;
    open(H, "<", "$dir/../../src/tool_help.h") or
        die "cannot find the header file";
    while(<H>) {
        if(/^\#define CURLHELP_([A-Z0-9]*)/) {
            $cats{lc($1)}++;
        }
    }
    close(H);
    return %cats;
}

sub listhelp {
    my ($dir) = @_;
    my %cats = sourcecategories($dir);

    print <<HEAD
/***************************************************************************
 *                                  _   _ ____  _
 *  Project                     ___| | | |  _ \\| |
 *                             / __| | | | |_) | |
 *                            | (__| |_| |  _ <| |___
 *                             \\___|\\___/|_| \\_\\_____|
 *
 * Copyright (C) Daniel Stenberg, <daniel\@haxx.se>, et al.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution. The terms
 * are also available at https://curl.se/docs/copyright.html.
 *
 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
 * copies of the Software, and permit persons to whom the Software is
 * furnished to do so, under the terms of the COPYING file.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 * SPDX-License-Identifier: curl
 *
 ***************************************************************************/
#include "tool_setup.h"

#include "tool_help.h"

/*
 * DO NOT edit tool_listhelp.c manually.
 * This source file is generated with the following command in an autotools
 * build:
 *
 * "make listhelp"
 */

const struct helptxt helptext[] = {
HEAD
        ;
    foreach my $f (sort keys %helplong) {
        my $long = $f;
        my $short = $optlong{$long};
        my @categories = split ' ', $catlong{$long};
        my $bitmask = ' ';
        my $opt;

        if(defined($short) && $long) {
            $opt = "-$short, --$long";
        }
        elsif($long && !$short) {
            $opt = "    --$long";
        }
        for my $i (0 .. $#categories) {
            if(!$cats{ $categories[$i] }) {
                printf STDERR "$f.md:ERROR: Unknown category '%s'\n",
                    $categories[$i];
                exit 3;
            }

            $bitmask .= 'CURLHELP_' . uc $categories[$i];
            # If not last element, append |
            if($i < $#categories) {
                $bitmask .= ' | ';
            }
        }
        $bitmask =~ s/(?=.{75}).{1,75}\|/$&\n   /g;
        my $arg = $arglong{$long};
        if($arg) {
            $opt .= " $arg";
        }
        my $desc = $helplong{$f};
        $desc =~ s/\"/\\\"/g; # escape double quotes

        my $line = sprintf "  { \"%s\",\n    \"%s\",\n   %s },\n", $opt, $desc, $bitmask;

        if(length($opt) > 78) {
            print STDERR "WARN: the --$long name is too long\n";
        }
        elsif(length($desc) > 78) {
            print STDERR "WARN: the --$long description is too long\n";
        }
        my $ifdef = "";
        my $endif = "";

        if($long =~ /^(ipfs|httpsig)/) {
            $ifdef = sprintf "#ifndef CURL_DISABLE_%s\n", uc($1);
            $endif = "#endif\n";
        }
        print $ifdef.$line.$endif;
    }
    print <<FOOT
  { NULL, NULL, 0 }
};
FOOT
        ;
}

sub listcats {
    my %allcats;
    foreach my $f (sort keys %helplong) {
        my @categories = split ' ', $catlong{$f};
        foreach (@categories) {
            $allcats{$_} = undef;
        }
    }
    my @categories;
    foreach my $key (keys %allcats) {
        push @categories, $key;
    }
    @categories = sort @categories;
    for my $i (0..$#categories) {
        printf("#define CURLHELP_%-10s (%s)\n",
               uc($categories[$i]), "1 << ${i}");
    }
}

sub listglobals {
    my ($dir, @files) = @_;
    my @globalopts;

    # Find all global options and output them
    foreach my $f (sort @files) {
        open(F, "<:crlf", "$dir/$f") or
            die "could not read $dir/$f";
        my $long;
        my $start = 0;
        while(<F>) {
            if(/^---/) {
                if(!$start) {
                    $start = 1;
                    next;
                }
                else {
                    last;
                }
            }
            if(/^Long: *(.*)/i) {
                $long = $1;
            }
            elsif(/^Scope: global/i) {
                push @globalopts, $long;
                last;
            }
        }
        close(F);
    }
    for my $e (0 .. $#globalopts) {
        $globals .= sprintf "%s--%s",  $e ? ($globalopts[$e + 1] ? ", " : " and ") : "",
            $globalopts[$e],;
    }
}

sub noext {
    my $in = $_[0];
    $in =~ s/\.md//;
    return $in;
}

sub sortnames {
    return noext($a) cmp noext($b);
}

sub mainpage {
    my ($dir, $manpage, @files) = @_;
    # $manpage is 1 for nroff, 0 for ASCII
    my $ret;
    my $fh;
    open($fh, "<:crlf", "$dir/mainpage.idx") or
        die "no $dir/mainpage.idx file";

    print <<HEADER
.\\" **************************************************************************
.\\" *                                  _   _ ____  _
.\\" *  Project                     ___| | | |  _ \\| |
.\\" *                             / __| | | | |_) | |
.\\" *                            | (__| |_| |  _ <| |___
.\\" *                             \\___|\\___/|_| \\_\\_____|
.\\" *
.\\" * Copyright (C) Daniel Stenberg, <daniel\@haxx.se>, et al.
.\\" *
.\\" * This software is licensed as described in the file COPYING, which
.\\" * you should have received as part of this distribution. The terms
.\\" * are also available at https://curl.se/docs/copyright.html.
.\\" *
.\\" * You may opt to use, copy, modify, merge, publish, distribute and/or sell
.\\" * copies of the Software, and permit persons to whom the Software is
.\\" * furnished to do so, under the terms of the COPYING file.
.\\" *
.\\" * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
.\\" * KIND, either express or implied.
.\\" *
.\\" * SPDX-License-Identifier: curl
.\\" *
.\\" **************************************************************************
.\\"
.\\" DO NOT EDIT. Generated by the curl project managen man page generator.
.\\"
.TH curl 1 "$date" "curl $version" "curl Manual"
HEADER
        if($manpage);

    while(<$fh>) {
        my $f = $_;
        chomp $f;
        if($f =~ /^#/) {
            # standard comment
            next;
        }
        if(/^%options/) {
            # output docs for all options
            foreach my $f (sort sortnames @files) {
                $ret += single($dir, $manpage, $f, 0);
            }
        }
        else {
            # render the file
            header($dir, $manpage, $f);
        }
    }
    close($fh);
    exit $ret if($ret);
}

sub showonly {
    my ($dir, $f) = @_;
    if(single($dir, 1, $f, 1)) {
        print STDERR "$f: failed\n";
    }
}

sub showprotocols {
    my %prots;
    foreach my $f (keys %optlong) {
        my @p = split(/ /, $protolong{$f});
        for my $p (@p) {
            $prots{$p}++;
        }
    }
    for(sort keys %prots) {
        printf "$_ (%d options)\n", $prots{$_};
    }
}

sub getargs {
    my ($dir, $f, @s) = @_;
    if($f eq "mainpage") {
        listglobals($dir, @s);
        mainpage($dir, 1, @s);
        return;
    }
    elsif($f eq "ascii") {
        listglobals($dir, @s);
        mainpage($dir, 0, @s);
        return;
    }
    elsif($f eq "listhelp") {
        listhelp($dir);
        return;
    }
    elsif($f eq "single") {
        showonly($dir, $s[0]);
        return;
    }
    elsif($f eq "protos") {
        showprotocols();
        return;
    }
    elsif($f eq "listcats") {
        listcats();
        return;
    }

    print "Usage: managen ".
        "[-d dir] <mainpage/ascii/listhelp/single FILE/protos/listcats> [files]\n";
}

#------------------------------------------------------------------------

my $dir = ".";
my $include = "../../include";
my $cmd = shift @ARGV || '';

while(1) {
    if($cmd eq "-d") {
        # specifies source directory
        $dir = shift @ARGV;
        $cmd = shift @ARGV;
    }
    elsif($cmd eq "-I") {
        # include path root
        $include = shift @ARGV;
        $cmd = shift @ARGV;
    }
    elsif($cmd eq "-c") {
        # Column width
        $colwidth = 0 + shift @ARGV;
        $cmd = shift @ARGV;
    }
    else {
        last;
    }
}

my @files = @ARGV; # the rest are the files

# can be overridden for releases
if($ENV{'CURL_MAKETGZ_VERSION'}) {
    $version = $ENV{'CURL_MAKETGZ_VERSION'};
}
else {
    open(INC, "<", "$include/curl/curlver.h") or die "no $include/curl/curlver.h";
    while(<INC>) {
        if($_ =~ /^#define LIBCURL_VERSION \"([0-9.]*)/) {
            $version = $1;
            last;
        }
    }
    close(INC);
}

# learn all existing options
indexoptions($dir, @files);

getargs($dir, $cmd, @files);

exit $error;
