#!/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 } gen_doc_func() { local sym="${1}" local doc="${2}" local shso="${3}" shift 3 local cmd= local args= local sect_name= local sect_syn= 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= sect_name=".SH NAME${LF}${sym}" sect_syn=".SH SYNOPSIS${LF}" if [ -n "${shso}" ]; then sect_syn="${sect_syn}.B . ${shso}${LF}.P${LF}" fi sect_syn="${sect_syn}.B ${sym}${LF}" 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="\\fB${opt}\\fP" else opt="\\fB${opt}\\fP \\fI${optarg}\\fP" fi sect_opts="${sect_opts}.TP${LF}${opt}${LF}" sect_opts="${sect_opts}${desc}${LF}" sect_syn="${sect_syn}[${opt}]${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}" case "${req}" in 'r'*|'R'*|'y'*|'Y'*) opd="\\fI${opd}\\fP";; *) opd="[\\fI${opd}\\fP]";; esac sect_syn="${sect_syn}${opd}${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_syn}" "${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 shso="${4}" local sym="${5}" local is_func="${6}" local doc="${7}" local out_dir="${8}" shift 8 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}" "${shso}" fi } >"${out_dir}/${sym}.3" } parse_docs() { local file="${1}" local source="${2}" local manual="${3}" local shso="${4}" local out_dir="${5}" shift 5 local date= local line= local doc='' local got_doc=false local is_func=false date="$("$(dirname "${0}")/mtime.sh" "${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}" "${shso}" \ "${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"] -S Show how to load a shell shared object file (". ") in "SYNOPSIS" sections -b Look for listed source files in [default: the current working directory] -d Write manual pages in [default: the current working directory] -t List all documented symbols in 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 shso='' local base_dir='.' local out_dir='.' local tags='' local f= while getopts 'hVs:m:S:b:d:t:' opt; do case "${opt}" in 'h') help exit ;; 'V') version exit ;; 's') source="${OPTARG}" ;; 'm') manual="${OPTARG}" ;; 'S') shso="${OPTARG}" ;; 'b') base_dir="${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 mkdir -p "$(dirname "${tags}")" exec 3>"${tags}" fi mkdir -p "${out_dir}" for f in "${@}"; do parse_docs "${base_dir}/${f}" \ "${source}" "${manual}" "${shso}" "${out_dir}" done if [ -n "${tags}" ]; then exec 3>&- fi } main "${@}"