492 lines
13 KiB
Bash
Executable File
492 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
function usage {
|
|
cat <<'EoT'
|
|
NAME
|
|
manpage - format an ascii text file into man page format
|
|
|
|
SYNOPSIS
|
|
manpage [-PR] [-d pdate] [-r rel] [-v vol] [-s sect] [-t title]
|
|
[-w ncols] [filename]
|
|
|
|
DESCRIPTION
|
|
manpage converts its input into the man(7) macros as used by man(1),
|
|
but without the user needed to understand any formatting syntax.
|
|
It is designed to process a flat ascii file that roughly appears in the format
|
|
desired, adding the specific flourishes that distinguish a readable man page:
|
|
|
|
- font changes for names and arguments throughout the document
|
|
- automatic recognition of command, C, C++, and Java names, types and arguments
|
|
- automatic recognition of paragraphs, list formats, sections, and non-formatted displays
|
|
- constant width fonts for code and displays
|
|
- cross-references highlighted
|
|
- pass-through of nroff formatting commands
|
|
|
|
The input resembles the man page to be formatted, with just a few rules of presentation:
|
|
Sections ~
|
|
Section names are input lines in all upper case. If a section name begins
|
|
with spaces, it starts a subsection.
|
|
Paragraphs ~
|
|
A blank line between text marks a paragraph. All text in the same
|
|
paragraph must be left-aligned relative to itself.
|
|
Line Breaks ~
|
|
A twiddle (~) at the end of a line is consumed and replaced
|
|
with a line break. This is especially useful in tag lists (this section).
|
|
Displays ~
|
|
These are sections not to be formatted further. They are introduced
|
|
by a preceding blank line and indented relative to the surrounding text.
|
|
Displays are output in constant width font, and all code escapes may
|
|
be presented naturally:
|
|
|
|
printf("Hello, World\n");
|
|
|
|
Tag Lists ~
|
|
The tag is identified by three (3) or more spaces between itself
|
|
and its description. These rules are tag lists. Note especially
|
|
the trailing spaces on the tags in this section.
|
|
Bullet Lists ~
|
|
These are identified by a leading dash (-), dot (.), or star (*).
|
|
Numbered Lists ~
|
|
These begin with a number and a dot; e.g. "3. Number three".
|
|
Cross References ~
|
|
This is a word followed by a number in parenthesis; e.g. man(1).
|
|
Bold / Italic ~
|
|
A word surrounded by underscores (_) is rendered in _italic_;
|
|
and by stars (*) is rendered as *boldface*.
|
|
|
|
There are two sections handled specially:
|
|
NAME
|
|
This is the name of the command or function, followed by a terse,
|
|
one-line summary of its primary use.
|
|
SYNOPSIS
|
|
This section is scanned for names, arguments, and flags. Names found
|
|
in this area are remembered, and each occurance throughout the document
|
|
are presented in bold. Arguments are similarly detected and remembered,
|
|
and presented in italics. Other text in this area is presented in
|
|
a constant width font.
|
|
|
|
OPTIONS
|
|
-d pdate set the man page date
|
|
-r rel set the release name
|
|
-v vol set the volume name
|
|
-s sect set the section name
|
|
-t title set the man page title
|
|
-w ncols force the output to be ncols wide
|
|
-P PostScript output
|
|
-R roff output
|
|
|
|
EXAMPLES
|
|
This usage is in the proper format for manpage. Try:
|
|
|
|
manpage -h | manpage -tmanpage -s1
|
|
|
|
To view the nroff(1) formatted page, perhaps for inclusion in the system man pages:
|
|
|
|
manpage -h | manpage -tmanpage -s1 -R > manpage.1
|
|
|
|
ENVIRONMENT
|
|
PAGER defaults to less(1); used if neither -P nor -R is specified as arguments
|
|
MANPATH input .man files searched in current dir, then HOME/man, then MANPATH
|
|
ORG name of organization (e.g. $ORG Reference Manual)
|
|
|
|
SEE ALSO
|
|
man(1), man(7), groff(1), less(1), txt2man(1)
|
|
|
|
PORTABILITY
|
|
This program uses generic bourne shell and awk syntax, POSIX compatible systems should work.
|
|
|
|
BUGS
|
|
Code displays that span page breaks do not restore the troff(1) escape
|
|
during header / footer processing.
|
|
|
|
AUTHOR
|
|
Dave Burton <ioccc@snox.net>
|
|
|
|
manpage(1) input format is inspired by txt2man(1) by Marc Vertes <mvertes@free.fr>,
|
|
although the code is a clean redesign from first principles.
|
|
EoT
|
|
}
|
|
|
|
function getwidth {
|
|
stty -a 2>/dev/null |
|
|
if test "$(uname)" == "Darwin"; then
|
|
awk '/columns/{for(i=1;i<=NF;++i)if($i~/columns/)print$(i-1)+0}'
|
|
else
|
|
awk '/columns/{for(i=1;i<=NF;++i)if($i~/columns/)print$(i+1)+0}'
|
|
fi
|
|
}
|
|
|
|
function scan {
|
|
MAN_PATH=${MANPATH:="/opt/homebrew/share/man:/usr/local/share/man:/usr/local/man:/usr/share/man:/opt/X11/share/man"}
|
|
MAN_SET=$(echo "$MAN_PATH" | tr : '\n')
|
|
for dir in . "$HOME/man" "$MAN_SET"; do
|
|
test -r "$dir/$1" && { echo "$dir/$1"; break; }
|
|
done
|
|
}
|
|
|
|
function searchfor {
|
|
# examine the current dir for name, name.man, or scan a searchpath for name.man
|
|
test "$1" || { echo ""; return; }
|
|
filename="$1"
|
|
if ! test -r "$filename" || test -x "$filename"; then
|
|
expr "$filename" : '.*.man' >/dev/null 2>&1 || filename="$filename.man"
|
|
result=$(scan "$filename")
|
|
if ! test "$result"; then
|
|
echo "cannot find $filename in curdir, $HOME/man or MANPATH" >&2
|
|
exit 1
|
|
fi
|
|
filename="$result"
|
|
fi
|
|
echo "$filename"
|
|
}
|
|
|
|
width=0
|
|
export sys
|
|
sys=("$uname" -s)
|
|
date=
|
|
rel=
|
|
vol=
|
|
sect=
|
|
title=
|
|
post=
|
|
|
|
args="?hPRd:r:v:s:t:w:"
|
|
while getopts "$args" opt
|
|
do
|
|
case $opt in
|
|
d) date=$OPTARG;;
|
|
r) rel=$OPTARG;;
|
|
v) vol=$OPTARG;;
|
|
s) sect=$OPTARG;;
|
|
t) title=$OPTARG;;
|
|
w) width=$OPTARG;;
|
|
P) post="groff -man -Tps";;
|
|
R) post="cat";;
|
|
\?) echo "usage: $0 [-$args] [filename]" >&2; exit 1;;
|
|
h|*) usage; exit 1;;
|
|
esac
|
|
done
|
|
shift $((OPTIND-1))
|
|
|
|
date=${date:-$(date +'%d %B %Y')}
|
|
|
|
# format and display the page now
|
|
if test "$width" -eq 0 -a ! "$post" ; then
|
|
width=$(getwidth)
|
|
fi
|
|
|
|
# -is for pre-el-capitan
|
|
: "${post:=groff -man -Tascii | ${PAGER:-less -R}}"
|
|
|
|
# seek for it, perhaps without the .man extension
|
|
filename=$(searchfor "$1") || exit; shift;
|
|
# echo $filename; exit
|
|
|
|
# intuit the title, section and volume from the input file
|
|
if ! test "$title" && test "$filename"; then
|
|
title=${filename##*/}; title=${title%.man}
|
|
title=$(echo "$title" | tr '[:lower:]' '[:upper:]')
|
|
if sed '/DESCRIPTION/q' "$filename" | grep -E -q "#include|.import"; then
|
|
sect=${sect:-3}
|
|
vol=${vol:-"$ORG Programmer's Manual"}
|
|
else
|
|
sect=${sect:-1}
|
|
vol=${vol:-"$ORG Reference Manual"}
|
|
fi
|
|
fi
|
|
|
|
# --------------------------------------------------
|
|
# the formatting engine
|
|
|
|
expand "$filename" |
|
|
awk -v width="$width" -v th="$title $sect \"$date\" \"$rel\" \"$vol\"" '
|
|
BEGIN {
|
|
if (width+0 > 0) {
|
|
# adjust output to a specific width; useful as man(1) replacement
|
|
cpi = (width+0)/11
|
|
print ".ll " cpi "i"
|
|
print ".nr LL " cpi "i"
|
|
}
|
|
print ".TH", th
|
|
print ".if n .na\n.if n .nh"
|
|
block = 0 # depth of the block display
|
|
list = 0 # depth of the list indent
|
|
indent = 0 # current RS indent level
|
|
rs[indent] = 0 # array of starting indents
|
|
lw = 0 # leading whitespace
|
|
blank = 0 # NR of last blank line
|
|
}
|
|
|
|
function bold(w) { return "\\fB" w "\\fP" }
|
|
function italic(w) { return "\\fI" w "\\fP" }
|
|
|
|
function before(s) { return substr(s, 1, RSTART-1) }
|
|
function after(s) { return substr(s, RSTART+RLENGTH) }
|
|
|
|
function add(t, r) {
|
|
gsub(/\./, "\\.", t)
|
|
pattern[t] = r
|
|
}
|
|
|
|
# replace logic is W*P, but both W (words) and P (patterns) are small,
|
|
# and this is the only way to portably, reliably detect substitutable patterns
|
|
# within the larger text
|
|
#
|
|
function replace(dummy, i,count,other,pat,re) {
|
|
other = "[^-_A-Za-z0-9]+"
|
|
for (i=1; i<=NF; ++i) {
|
|
count = 0
|
|
for (pat in pattern) {
|
|
# re = "^" pat "$|^" other pat "$|^" pat other "$|^" other pat other "$"
|
|
re = "^" pat "$|" other pat "$|^" pat other "|" other pat other
|
|
if ($i ~ re) {
|
|
count = sub(pat, pattern[pat], $i)
|
|
break;
|
|
}
|
|
}
|
|
|
|
# one-time, in place bold, italic, and cross-ref
|
|
if (section != "SYNOPSIS" && count == 0) {
|
|
# match foo(1) or foo(1pkg) not foo(1 2) or foo(1, 2) or foo(int)
|
|
# but not in a see also section
|
|
if ((section != "SEE") && # aka SEE ALSO
|
|
($i ~ /[_A-Za-z0-9]+\([0-9][A-Za-z0-9]*\)/)) {
|
|
a = index($i, "(")
|
|
$i = bold(substr($i, 1, a-1)) substr($i, a)
|
|
}
|
|
else if (match($i, "[A-Za-z]_.*_[A-Za-z]"))
|
|
;
|
|
else if (match($i, "_.*_"))
|
|
$i = before($i) italic(substr($i, RSTART+1, RLENGTH-2)) after($i)
|
|
else if (match($i, "\\*.*\\*"))
|
|
$i = before($i) bold(substr($i, RSTART+1, RLENGTH-2)) after($i)
|
|
}
|
|
}
|
|
if ($NF == "~") {
|
|
$NF = ""
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
function paragraph() {
|
|
print ".PP"
|
|
list = lw # reset list on a paragraph
|
|
tplen = ""
|
|
}
|
|
|
|
function restore() {
|
|
if (blank == NR-1) # restore the blank line to the display
|
|
print ""
|
|
n = lw - block
|
|
if (n==0) n=1 # avoid eating leading dots in displays
|
|
printf "%*s", n, " " # restore leading spaces in displays
|
|
}
|
|
|
|
function block_start() {
|
|
block = lw
|
|
blank = 0
|
|
paragraph()
|
|
print ".RS " block
|
|
print ".eo\n.nf\n.fam C"
|
|
}
|
|
|
|
function block_end() {
|
|
if (block > 0) {
|
|
block = 0
|
|
print ".ec\n.fi\n.fam T"
|
|
print ".RE"
|
|
}
|
|
}
|
|
|
|
function reset_indent() {
|
|
rs[0] = indent = 0
|
|
}
|
|
|
|
# pass through nroff formatting commands
|
|
/^\..+/ {
|
|
print $0
|
|
next
|
|
}
|
|
|
|
# remember leading whitespace, strip blank lines
|
|
{
|
|
lw = match($0, /[^ ]/)
|
|
if (lw == 0) {
|
|
blank = NR
|
|
next
|
|
} else {
|
|
$0 = substr($0, lw)
|
|
# sub(/^ */, "")
|
|
# sub(/ *$/, "") # prevents TP match if desc on next line
|
|
}
|
|
--lw
|
|
if (rs[indent] == 0)
|
|
rs[indent] = lw
|
|
}
|
|
|
|
# new section (must end with a cap letter to avoid tag list match)
|
|
/^[A-Z ]+[A-Z]$/ {
|
|
if (section == "SYNOPSIS")
|
|
print ".fi"
|
|
block_end()
|
|
reset_indent()
|
|
list = 0
|
|
tplen = ""
|
|
section = $1
|
|
print (lw>0) ? ".SS" : ".SH", $0
|
|
if (section == "SYNOPSIS") {
|
|
print ".nf"
|
|
print ".fam C"
|
|
}
|
|
if (section == "SEE")
|
|
print ".nf"
|
|
next
|
|
}
|
|
|
|
section == "NAME" && name == "" && $1 != "" {
|
|
name = $1
|
|
# add(name, bold(name))
|
|
sub(/ - /, " \\- ")
|
|
}
|
|
|
|
section == "SYNOPSIS" {
|
|
if (format == "") {
|
|
block = lw # this is a pseudo-block; see new section
|
|
if ($0 ~ /\(/) format = "code"
|
|
else if ($1 == "struct" || $2 == "struct") format = "struct"
|
|
else if ($1 == "union" || $2 == "union") format = "struct"
|
|
else if ($1 == "class" || $2 == "class") format = "class"
|
|
else if ($1 !~ /#|import|typedef|enum/) format = "cmd"
|
|
}
|
|
if (format == "cmd") {
|
|
# cmd [-a N] [-b N] [-m ab] [-p S]
|
|
# [-r] [-qst] @interface ip:port [filename ...]
|
|
i = 1
|
|
if ($1 !~ /^\[/) {
|
|
++i
|
|
a = $1
|
|
gsub("-", "\\-", a)
|
|
add(a, bold(a))
|
|
}
|
|
for (; i<=NF; ++i) {
|
|
a = $i
|
|
gsub(/[\[\]]/, "", a) # remove all brackets
|
|
if (a == "-") ;
|
|
else if (a ~ /^-./) add(a, bold("\\" a))
|
|
else if (a !~ /\.\.\./) add(a, italic(a))
|
|
}
|
|
} else if (format == "code") {
|
|
# void foo()
|
|
# void foo(int);
|
|
# void foo(int a);
|
|
# void foo(int a, char *b);
|
|
# void *foo(char **b, int *a, ...)
|
|
# void (*signal(int sig, void (*func)(int)))(int);
|
|
# ssize_t readv(int d, const struct iovec *iov, int iovcnt);
|
|
if ($1 != "typedef")
|
|
for (i = 1; i<=NF; ++i) {
|
|
a = $i
|
|
gsub("[\\*]", "", a) # dont want to mess with pointers
|
|
if (match(a, "[A-Za-z0-9_]+\\(")) {
|
|
a = substr(a, RSTART, RLENGTH-1)
|
|
add(a, bold(a))
|
|
} else if (match(a, "[A-Za-z0-9_]+[,\\)]")) {
|
|
a = substr(a, RSTART, RLENGTH-1)
|
|
add(a, italic(a))
|
|
}
|
|
}
|
|
} else if (format == "struct") {
|
|
if (match($0, "[A-Za-z0-9_]+[ ]*{")) {
|
|
a = substr($0, RSTART)
|
|
sub("[ ]*{", "", a);
|
|
add(a, bold(a));
|
|
}
|
|
format = "code"
|
|
} else if (format == "class") {
|
|
# public class Foo extends Bar implements Snox, Blah, Nuts {
|
|
# so remove from extends or implements, play as if C++
|
|
a = $0
|
|
if (match(a, "extends|implements"))
|
|
a = substr(a, 1, RSTART-1)
|
|
if (match(a, "[A-Za-z0-9_]+[ ]*{")) {
|
|
a = substr(a, RSTART)
|
|
sub("[ ]*{", "", a);
|
|
add(a, bold(a));
|
|
format = "code"
|
|
}
|
|
}
|
|
replace()
|
|
}
|
|
|
|
block == 0 {
|
|
if (lw > rs[indent]+2) {
|
|
if (blank == NR-1)
|
|
block_start()
|
|
else if (lw > list) {
|
|
# XXX list?
|
|
rs[++indent] = lw;
|
|
print ".RS"
|
|
}
|
|
} else while (lw < rs[indent]) {
|
|
print ".RE"
|
|
--indent
|
|
list = 0
|
|
tplen = ""
|
|
}
|
|
}
|
|
|
|
block > 0 {
|
|
if (lw < block)
|
|
block_end()
|
|
else {
|
|
restore()
|
|
print $0
|
|
next
|
|
}
|
|
}
|
|
|
|
{
|
|
# if replace before matching, awk collapses multiple spaces into one....
|
|
if (match($0, /[^ ] +/)) { # tag description
|
|
list = lw + RSTART + RLENGTH
|
|
tag = substr($0, 1, RSTART)
|
|
def = substr($0, RSTART + RLENGTH)
|
|
$0 = tag
|
|
if (tplen == "" && def !~ /[.~]/)
|
|
tplen = list-3
|
|
replace()
|
|
print ".TP " tplen "\n" bold($0)
|
|
$0 = def
|
|
|
|
} else if (match($0, /^[o\.\*+] +/)) { # bullet list
|
|
list = lw + RSTART + RLENGTH
|
|
print ".IP \\(bu 3"
|
|
$0 = substr($0, RLENGTH+1)
|
|
|
|
} else if (match($0, /^- +/)) { # dash list
|
|
list = lw + RSTART + RLENGTH
|
|
print ".IP \\(em 3"
|
|
$0 = substr($0, RLENGTH+1)
|
|
|
|
} else if (match($0, /^[0-9]+\. +/)) { # number list
|
|
list = lw + RSTART + RLENGTH
|
|
print ".IP " $1 " 3"
|
|
$0 = substr($0, RLENGTH+1)
|
|
|
|
} else {
|
|
# the lists above supply their own vertical separation
|
|
if (blank == NR-1)
|
|
paragraph()
|
|
}
|
|
|
|
# if there is anything left to print...
|
|
if (length $0 > 0) {
|
|
br = replace();
|
|
if (length $0 > 0)
|
|
print $0
|
|
if (br) print ".br"
|
|
}
|
|
}
|
|
' | eval "$post"
|