summaryrefslogtreecommitdiffstats
path: root/tools/shman.sh
diff options
context:
space:
mode:
Diffstat (limited to 'tools/shman.sh')
-rw-r--r--tools/shman.sh282
1 files changed, 282 insertions, 0 deletions
diff --git a/tools/shman.sh b/tools/shman.sh
new file mode 100644
index 0000000..7cd8140
--- /dev/null
+++ b/tools/shman.sh
@@ -0,0 +1,282 @@
+#!/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 <http://www.gnu.org/licenses/>.
+
+# 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}
+}
+
+format_fonts()
+{
+ local str="${1}"
+ shift 1
+
+ printf '%s\n' "${str}" | sed '
+ s/[*][*]\(.*\)[*][*]/\\fB\1\\fP/g;
+ s/_\(.*\)_/\\fI\1\\fP/g;
+ '
+}
+
+gen_doc_func()
+{
+ local sym="${1}"
+ local doc="${2}"
+ shift 2
+ local cmd=
+ local args=
+ local sect_name=
+ local sect_desc=
+ local sect_ret=
+
+ sect_name=".SH NAME${LF}${sym}"
+ while read -r cmd args; do
+ case "${cmd}" in
+ 'brief')
+ args="$(format_fonts "${args}")"
+ sect_name="${sect_name} - ${args}"
+ ;;
+ 'details')
+ args="$(format_fonts "${args}")"
+ sect_desc=".SH DESCRIPTION${LF}${args}"
+ ;;
+ 'return')
+ args="$(format_fonts "${args}")"
+ sect_ret=".SH RETURN VALUE${LF}${args}"
+ ;;
+ esac
+ done <<-EOF
+ ${doc}
+ EOF
+
+ printf '%s\n\n' "${sect_name}" "${sect_desc}" "${sect_ret}"
+}
+
+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
+ '##'*)
+ ## Strip "##" and leading and trailing whitespace
+ #read -r line <<-EOF
+ # ${line#'##'}
+ # EOF
+ 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 ...] <file>...\n' "${0}"
+}
+
+help()
+{
+ usage
+ cat <<EOF
+Options:
+ -h Display this information
+ -V Display version information
+ -s <source> The source of the manual pages, e.g. a package name and version
+ [default: "Shell"]
+ -m <manual> The title of the manual [default: "Shell Functions"]
+ -d <out_dir> The directory in which to write manual pages [default: the
+ current working directory]
+ -t <tags> A file in which to list all documented symbols
+EOF
+}
+
+version()
+{
+ cat <<EOF
+shman ${VERSION}
+Copyright (C) 2018 Patrick McDermott
+License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
+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 "${@}"