#!/usr/bin/env perl #*************************************************************************** # _ _ ____ _ # Project ___| | | | _ \| | # / __| | | | |_) | | # | (__| |_| | _ <| |___ # \___|\___/|_| \_\_____| # # Copyright (C) Daniel Stenberg, , 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 Converts a curldown file to nroff (manpage). =end comment =cut use strict; use warnings; my $cd2nroff = "0.1"; # to keep check my $dir; my $extension; my $keepfilename; while(@ARGV) { if($ARGV[0] eq "-d") { shift @ARGV; $dir = shift @ARGV; } elsif($ARGV[0] eq "-e") { shift @ARGV; $extension = shift @ARGV; } elsif($ARGV[0] eq "-k") { shift @ARGV; $keepfilename = 1; } elsif($ARGV[0] eq "-h") { print < Write the output to the file name from the meta-data in the specified directory, instead of writing to stdout -e If -d is used, this option can provide an added "extension", arbitrary text really, to append to the file name. -h This help text, -v Show version then exit HELP ; exit 0; } elsif($ARGV[0] eq "-v") { print "cd2nroff version $cd2nroff\n"; exit 0; } else { last; } } 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; sub outseealso { my (@sa) = @_; my $comma = 0; my @o; push @o, ".SH SEE ALSO\n"; for my $s (sort @sa) { push @o, sprintf "%s.BR $s", $comma ? ",\n": ""; $comma = 1; } push @o, "\n"; return @o; } sub outprotocols { my (@p) = @_; my $comma = 0; my @o; push @o, ".SH PROTOCOLS\n"; if($p[0] eq "TLS") { push @o, "This functionality affects all TLS based protocols: HTTPS, FTPS, IMAPS, POP3S, SMTPS etc."; } else { my @s = sort @p; push @o, "This functionality affects "; for my $e (sort @s) { push @o, sprintf "%s%s", $comma ? (($e eq $s[-1]) ? " and " : ", "): "", lc($e); $comma = 1; } if($#s == 0) { if($s[0] eq "All") { push @o, " supported protocols"; } else { push @o, " only"; } } } push @o, "\n"; return @o; } sub outtls { my (@t) = @_; my $comma = 0; my @o; if($t[0] eq "All") { push @o, "\nAll TLS backends support this option."; } else { push @o, "\nThis option works only with the following TLS backends:\n"; my @s = sort @t; for my $e (@s) { push @o, sprintf "%s$e", $comma ? (($e eq $s[-1]) ? " and " : ", "): ""; $comma = 1; } } push @o, "\n"; return @o; } my %knownprotos = ( 'DICT' => 1, 'FILE' => 1, 'FTP' => 1, 'FTPS' => 1, 'GOPHER' => 1, 'GOPHERS' => 1, 'HTTP' => 1, 'HTTPS' => 1, 'IMAP' => 1, 'IMAPS' => 1, 'LDAP' => 1, 'LDAPS' => 1, 'MQTT' => 1, 'POP3' => 1, 'POP3S' => 1, 'RTMP' => 1, 'RTMPS' => 1, 'RTSP' => 1, 'SCP' => 1, 'SFTP' => 1, 'SMB' => 1, 'SMBS' => 1, 'SMTP' => 1, 'SMTPS' => 1, 'TELNET' => 1, 'TFTP' => 1, 'WS' => 1, 'WSS' => 1, 'TLS' => 1, 'TCP' => 1, 'All' => 1 ); my %knowntls = ( 'BearSSL' => 1, 'GnuTLS' => 1, 'mbedTLS' => 1, 'OpenSSL' => 1, 'rustls' => 1, 'Schannel' => 1, 'Secure Transport' => 1, 'wolfSSL' => 1, 'All' => 1, ); sub single { my @seealso; my @proto; my @tls; my $d; my ($f)=@_; my $copyright; my $errors = 0; my $fh; my $line; my $list; my $tlslist; my $section; my $source; my $addedin; my $spdx; my $start = 0; my $title; if(defined($f)) { if(!open($fh, "<:crlf", "$f")) { print STDERR "cd2nroff failed to open '$f' for reading: $!\n"; return 1; } } else { $f = "STDIN"; $fh = \*STDIN; binmode($fh, ":crlf"); } while(<$fh>) { $line++; if(!$start) { if(/^---/) { # header starts here $start = 1; } next; } if(/^Title: *(.*)/i) { $title=$1; } elsif(/^Section: *(.*)/i) { $section=$1; } elsif(/^Source: *(.*)/i) { $source=$1; } elsif(/^See-also: +(.*)/i) { $list = 1; # 1 for see-also push @seealso, $1; } elsif(/^See-also: */i) { if($seealso[0]) { print STDERR "$f:$line:1:ERROR: bad See-Also, needs list\n"; return 2; } $list = 1; # 1 for see-also } elsif(/^Protocol:/i) { $list = 2; # 2 for protocol } elsif(/^TLS-backend:/i) { $list = 3; # 3 for TLS backend } elsif(/^Added-in: *(.*)/i) { $addedin=$1; if(($addedin !~ /^[0-9.]+[0-9]\z/) && ($addedin ne "n/a")) { print STDERR "$f:$line:1:ERROR: invalid version number in Added-in line: $addedin\n"; return 2; } } elsif(/^ +- (.*)/i) { # the only lists we support are see-also and protocol if($list == 1) { push @seealso, $1; } elsif($list == 2) { push @proto, $1; } elsif($list == 3) { push @tls, $1; } else { print STDERR "$f:$line:1:ERROR: list item without owner?\n"; return 2; } } # REUSE-IgnoreStart elsif(/^C: (.*)/i) { $copyright=$1; } elsif(/^SPDX-License-Identifier: (.*)/i) { $spdx=$1; } # REUSE-IgnoreEnd elsif(/^---/) { # end of the header section if(!$title) { print STDERR "$f:$line:1:ERROR: no 'Title:' in $f\n"; return 1; } if(!$section) { print STDERR "$f:$line:1:ERROR: no 'Section:' in $f\n"; return 2; } if(!$source) { print STDERR "$f:$line:1:ERROR: no 'Source:' in $f\n"; return 2; } if(!$addedin) { print STDERR "$f:$line:1:ERROR: no 'Added-in:' in $f\n"; return 2; } if(!$seealso[0]) { print STDERR "$f:$line:1:ERROR: no 'See-also:' present\n"; return 2; } if(!$copyright) { print STDERR "$f:$line:1:ERROR: no 'C:' field present\n"; return 2; } if(!$spdx) { print STDERR "$f:$line:1:ERROR: no 'SPDX-License-Identifier:' field present\n"; return 2; } if($section == 3) { if(!$proto[0]) { printf STDERR "$f:$line:1:ERROR: missing Protocol:\n"; exit 2; } my $tls = 0; for my $p (@proto) { if($p eq "TLS") { $tls = 1; } if(!$knownprotos{$p}) { printf STDERR "$f:$line:1:ERROR: invalid protocol used: $p:\n"; exit 2; } } # This is for TLS, require TLS-backend: if($tls) { if(!$tls[0]) { printf STDERR "$f:$line:1:ERROR: missing TLS-backend:\n"; exit 2; } for my $t (@tls) { if(!$knowntls{$t}) { printf STDERR "$f:$line:1:ERROR: invalid TLS backend: $t:\n"; exit 2; } } } } last; } else { chomp; print STDERR "$f:$line:1:ERROR: unrecognized header keyword: '$_'\n"; $errors++; } } if(!$start) { print STDERR "$f:$line:1:ERROR: no header present\n"; return 2; } my @desc; my $quote = 0; my $blankline = 0; my $header = 0; # cut off the leading path from the file name, if any $f =~ s/^(.*[\\\/])//; push @desc, ".\\\" generated by cd2nroff $cd2nroff from $f\n"; push @desc, ".TH $title $section \"$date\" $source\n"; while(<$fh>) { $line++; $d = $_; if($quote) { if($quote == 4) { # remove the indentation if($d =~ /^ (.*)/) { push @desc, "$1\n"; next; } else { # end of quote $quote = 0; push @desc, ".fi\n"; next; } } if(/^~~~/) { # end of quote $quote = 0; push @desc, ".fi\n"; next; } # convert single backslahes to doubles $d =~ s/\\/\\\\/g; # lines starting with a period needs it escaped $d =~ s/^\./\\&./; push @desc, $d; next; } # remove single line HTML comments $d =~ s///g; # **bold** $d =~ s/\*\*(\S.*?)\*\*/\\fB$1\\fP/g; # *italics* $d =~ s/\*(\S.*?)\*/\\fI$1\\fP/g; if($d =~ /[^\\][\<\>]/) { print STDERR "$f:$line:1:ERROR: un-escaped < or > used\n"; $errors++; } # convert backslash-'<' or '> to just the second character $d =~ s/\\([<>])/$1/g; # mentions of curl symbols with manpages use italics by default $d =~ s/((lib|)curl([^ ]*\(3\)))/\\fI$1\\fP/gi; # backticked becomes italics $d =~ s/\`(.*?)\`/\\fI$1\\fP/g; if(/^## (.*)/) { my $word = $1; # if there are enclosing quotes, remove them first $word =~ s/[\"\'\`](.*)[\"\'\`]\z/$1/; # enclose in double quotes if there is a space present if($word =~ / /) { push @desc, ".IP \"$word\"\n"; } else { push @desc, ".IP $word\n"; } $header = 1; } elsif(/^##/) { # end of IP sequence push @desc, ".PP\n"; $header = 1; } elsif(/^# (.*)/) { my $word = $1; # if there are enclosing quotes, remove them first $word =~ s/[\"\'](.*)[\"\']\z/$1/; if($word eq "PROTOCOLS") { print STDERR "$f:$line:1:WARN: PROTOCOLS section in source file\n"; } elsif($word eq "AVAILABILITY") { print STDERR "$f:$line:1:WARN: AVAILABILITY section in source file\n"; } elsif($word eq "%PROTOCOLS%") { # insert the generated PROTOCOLS section push @desc, outprotocols(@proto); if($proto[0] eq "TLS") { push @desc, outtls(@tls); } $header = 1; next; } elsif($word eq "%AVAILABILITY%") { if($addedin ne "n/a") { # insert the generated AVAILABILITY section push @desc, ".SH AVAILABILITY\n"; push @desc, "Added in curl $addedin\n"; } $header = 1; next; } push @desc, ".SH $word\n"; $header = 1; } elsif(/^~~~c/) { # start of a code section, not indented $quote = 1; push @desc, "\n" if($blankline && !$header); $header = 0; push @desc, ".nf\n"; } elsif(/^~~~/) { # start of a quote section; not code, not indented $quote = 1; push @desc, "\n" if($blankline && !$header); $header = 0; push @desc, ".nf\n"; } elsif(/^ (.*)/) { # quoted, indented by 4 space $quote = 4; push @desc, "\n" if($blankline && !$header); $header = 0; push @desc, ".nf\n$1\n"; } elsif(/^[ \t]*\n/) { # count and ignore blank lines $blankline++; } else { # don't output newlines if this is the first content after a # header push @desc, "\n" if($blankline && !$header); $blankline = 0; $header = 0; # quote minuses in the output $d =~ s/([^\\])-/$1\\-/g; # replace single quotes $d =~ s/\'/\\(aq/g; # handle double quotes first on the line $d =~ s/^(\s*)\"/$1\\&\"/; # lines starting with a period needs it escaped $d =~ s/^\./\\&./; if($d =~ /^(.*) /) { printf STDERR "$f:$line:%d:ERROR: 2 spaces detected\n", length($1); $errors++; } if($d =~ /^[ \t]*\n/) { # replaced away all contents $blankline= 1; } else { push @desc, $d; } } } if($fh != \*STDIN) { close($fh); } push @desc, outseealso(@seealso); if($dir) { if($keepfilename) { $title = $f; $title =~ s/\.[^.]*$//; } my $outfile = "$dir/$title.$section"; if(defined($extension)) { $outfile .= $extension; } if(!open(O, ">", $outfile)) { print STDERR "Failed to open $outfile : $!\n"; return 1; } print O @desc; close(O); } else { print @desc; } return $errors; } if(@ARGV) { for my $f (@ARGV) { my $r = single($f); if($r) { exit $r; } } } else { exit single(); }