#!/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=' ' FUNC_CMDS='brief option operand details return' 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_ret= local sect_pure= 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 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 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}" ;; 'return') sect_ret=".SH RETURN VALUE${LF}${args}" ;; 'purity') sect_pure=".SH SUBSHELL SAFETY AND SIDE EFFECTS" sect_pure="${sect_pure}${LF}${args}" ;; esac done <<-EOF ${doc} EOF printf '%s\n\n' "${sect_name}" "${sect_desc}" \ "${sect_opts}" "${sect_opds}" "${sect_ret}" "${sect_pure}" } 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 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}" local tags="${5}" shift 5 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}" 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 for f in "${@}"; do parse_docs "${f}" "${source}" "${manual}" "${out_dir}" "${tags}" done } main "${@}"