#!/bin/sh
#
# Shell command language manual generator
#
# Copyright (C) 2018 Patrick McDermott
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# Man, shman!
set -u
VERSION='0.1.0'
LF='
'
warn()
{
local fmt="${1}"
shift 1
printf "shman: Warning: ${fmt}\n" "${@}" >&2
}
get_mtime()
{
local file="${1}"
shift 1
local m=
local d=
local y=
local now_m=
local now_y=
read m d y <<-EOF
$(LC_ALL=POSIX ls -l "${file}" | cut -d ' ' -f 6-8)
EOF
case "${m}" in
'Jan') m=1;; 'Feb') m=2;; 'Mar') m=3;; 'Apr') m=4;;
'May') m=5;; 'Jun') m=6;; 'Jul') m=7;; 'Aug') m=8;;
'Sep') m=9;; 'Oct') m=10;; 'Nov') m=11;; 'Dec') m=12;;
esac
case "${y}" in *':'*)
read now_m now_y <<-EOF
$(date '+%m %Y')
EOF
now_m="${now_m#0}"
if [ ${now_m} -ge ${m} ]; then
y=${now_y}
else
y=$((${now_y} - 1))
fi
esac
printf '%d-%02d-%02d' ${y} ${m} ${d}
}
gen_doc_func()
{
local sym="${1}"
local doc="${2}"
shift 2
local cmd=
local args=
local sect_name=
local sect_desc=
local opt=
local optarg=
local desc=
local first_opt=true
local sect_opts=
local opd=
local req=
local first_opd=true
local sect_opds=
local sect_in=
local sect_ret=
local sect_out=
local sect_err=
local pure=
local sect_attr=
# TODO: SYNOPSIS
sect_name=".SH NAME${LF}${sym}"
while read -r cmd args; do
case "${cmd}" in
'brief')
sect_name="${sect_name} - ${args}"
;;
'details')
sect_desc=".SH DESCRIPTION${LF}${args}"
;;
'option')
read -r opt optarg desc <<-EOF
${args}
EOF
if ${first_opt}; then
sect_opts=".SH OPTIONS${LF}"
fi
first_opt=false
if [ "x${optarg}" = 'x-' ]; then
opt=".B ${opt}"
else
opt=".BI ${opt} \\ ${optarg}"
fi
sect_opts="${sect_opts}.TP${LF}${opt}${LF}"
sect_opts="${sect_opts}${desc}${LF}"
;;
'operand')
read -r opd req desc <<-EOF
${args}
EOF
if ${first_opd}; then
sect_opds=".SH OPERANDS${LF}"
fi
first_opd=false
sect_opds="${sect_opds}.TP${LF}.I ${opd}${LF}"
sect_opds="${sect_opds}${desc}${LF}"
;;
'stdin')
sect_in=".SH STDIN${LF}${args}"
;;
'return')
sect_ret=".SH RETURN VALUE${LF}${args}"
;;
'stdout')
sect_out=".SH STDOUT${LF}${args}"
;;
'stderr')
sect_err=".SH STDERR${LF}${args}"
;;
'pure')
read -r pure desc <<-EOF
${args}
EOF
sect_attr=".SH ATTRIBUTES${LF}"
sect_attr="${sect_attr}.TP${LF}"
sect_attr="${sect_attr}Subshell-safe: ${pure}"
sect_attr="${sect_attr}${LF}${desc}"
;;
esac
done <<-EOF
${doc}
EOF
printf '%s\n\n' "${sect_name}" "${sect_desc}" \
"${sect_opts}" "${sect_opds}" "${sect_in}" "${sect_ret}" \
"${sect_out}" "${sect_err}" "${sect_attr}"
}
gen_doc()
{
local date="${1}"
local source="${2}"
local manual="${3}"
local sym="${4}"
local is_func="${5}"
local doc="${6}"
local out_dir="${7}"
shift 7
local line=
local doc_joined=''
local sym_upper=
# Join command lines
while read -r line; do
case "${line}" in
'@'*)
doc_joined="${doc_joined}${LF}${line#@}"
;;
*)
doc_joined="${doc_joined} ${line}"
;;
esac
done <<-EOF
${doc}
EOF
doc="${doc_joined#${LF}}"
# Now each line in ${doc} should begin with a command name and thus be
# easier to parse.
sym_upper="$(printf '%s\n' "${sym}" | tr '[:lower:]' '[:upper:]')"
{
if ${is_func}; then
printf '.TH %s 3 "%s" "%s" "%s"\n\n' \
"${sym_upper}" "${date}" "${source}" "${manual}"
gen_doc_func "${sym}" "${doc}"
fi
} >"${out_dir}/${sym}.3"
}
parse_docs()
{
local file="${1}"
local source="${2}"
local manual="${3}"
local out_dir="${4}"
shift 4
local date=
local line=
local doc=''
local got_doc=false
local is_func=false
date="$(get_mtime "${file}")"
while IFS='' read -r line; do
case "${line}" in
'##'*)
doc="${doc}${line#\#\#}${LF}"
got_doc=true
continue
;;
'')
continue
;;
*'()') line="${line%()}" is_func=true;;
*'='*) line="${line%%=*}" is_func=false;;
*)
doc=''
got_doc=false
continue
;;
esac
case "${line}" in [!a-zA-Z]*|*[!a-zA-Z0-9_]*) continue;; esac
if ${got_doc}; then
gen_doc "${date}" "${source}" "${manual}" \
"${line}" ${is_func} "${doc}" "${out_dir}"
if [ -n "${tags}" ]; then
if ${is_func}; then
printf '%s()\n' "${line}" >&3
else
printf '%s\n' "${line}" >&3
fi
fi
doc=''
got_doc=false
elif ${is_func}; then
warn 'Undocumented function "%s"' "${line}"
fi
done <"${file}"
}
usage()
{
printf 'Usage: %s [option ...] ...\n' "${0}"
}
help()
{
usage
cat < The source of the manual pages, e.g. a package name and version
[default: "Shell"]
-m The title of the manual [default: "Shell Functions"]
-d The directory in which to write manual pages [default: the
current working directory]
-t A file in which to list all documented symbols
EOF
}
version()
{
cat <.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
EOF
}
main()
{
local opt=
local source='Shell'
local manual='Shell Functions'
local out_dir='.'
local tags=''
local f=
while getopts 'hVs:m:d:t:' opt; do
case "${opt}" in
'h')
help
exit
;;
'V')
version
exit
;;
's')
source="${OPTARG}"
;;
'm')
manual="${OPTARG}"
;;
'd')
out_dir="${OPTARG}"
;;
't')
tags="${OPTARG}"
;;
esac
done
shift $(($OPTIND - 1))
if [ ${#} -lt 1 ]; then
usage >&2
exit 1
fi
if [ -n "${tags}" ]; then
exec 3>"${tags}"
fi
for f in "${@}"; do
parse_docs "${f}" "${source}" "${manual}" "${out_dir}"
done
if [ -n "${tags}" ]; then
exec 3>&-
fi
}
main "${@}"