summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick Spek <p.spek@tyil.nl>2024-08-12 13:03:50 +0200
committerPatrick Spek <p.spek@tyil.nl>2024-08-12 13:03:50 +0200
commite91a4e7cc9646de16ac955321473d0e1237b4b60 (patch)
tree01b9630c57c1f9c31781b130ebc59b8b5f90a0b5
downloadbocs-e91a4e7cc9646de16ac955321473d0e1237b4b60.tar.gz
bocs-e91a4e7cc9646de16ac955321473d0e1237b4b60.tar.bz2
Initial commitHEADmaster
-rwxr-xr-xbocs.bash487
1 files changed, 487 insertions, 0 deletions
diff --git a/bocs.bash b/bocs.bash
new file mode 100755
index 0000000..59f2c3e
--- /dev/null
+++ b/bocs.bash
@@ -0,0 +1,487 @@
+#!/usr/bin/env bash
+
+## :bocs:
+## :section: 1
+## :heading: synopsis
+##
+## ```
+## bocs -h
+## bocs [-c <prefix>] [-n <name>] [-t <date>] [-v] <files...>
+## ```
+
+## :bocs:
+## :section: 1
+## :heading: description
+##
+## **bocs** is a small Bash script to assist in generating manpages for other
+## Bash programs from source code comments. It depends on having Bash
+## available, the basic POSIX utility programs, and _lowdown_ (a small program
+## to convert markdown into roff).
+
+## :bocs:
+## :section: 1
+## :heading: examples
+##
+## ## Generate bocs' own embedded manpages
+##
+## ```
+## bocs -n bocs -v ./bocs
+## ```
+
+## :bocs:
+## :section: 1
+## :heading: see also
+##
+## - bocs(3)
+
+## :bocs:
+## :section: X
+## :heading: authors
+##
+## - Patrick Spek `<p.spek@tyil.nl>`
+
+readonly WORKDIR="$(mktemp -d)"
+
+# TODO: Support manualing additional volumes? Maybe add a tag to specify a
+# volume, and an opt to generate manpages relating to that volume?
+main() {
+ local project
+ local date
+ local prefix="##"
+
+ # Show usage and exit if no positional args were passed
+ if (( $# < 1 ))
+ then
+ usage
+ exit 1
+ fi
+
+ # Parse opts
+ while getopts ":c:hn:t:v" opt
+ do
+ case "$opt" in
+ c)
+ prefix="$OPTARG"
+ ;;
+ h)
+ usage
+ exit 0
+ ;;
+ n)
+ project="$OPTARG"
+ ;;
+ t)
+ date="$OPTARG"
+ ;;
+ v)
+ export BOCS_VERBOSE=1
+ ;;
+ *)
+ printf "Unknown opt: %s\n" "$OPTARG" >&2
+ ;;
+ esac
+ done
+
+ shift $(( OPTIND - 1 ))
+
+ # Set defaults where needed
+ [[ -z $project ]] && project="bocs"
+ [[ -z $date ]] && date="$(date +"%Y-%m-%d")"
+
+ # Scan all files for blocs comments (Anything starting with ## with 0
+ # or more whitespace in front ot it), and split them into numbered
+ # blocks
+ prepare_scan "$prefix" "$@"
+ prepare_block_numbered
+
+ # Go over each block to get the section and title
+ for block in "$WORKDIR/block-"*".txt"
+ do
+ prepare_block_named "$block"
+ done
+
+ # Write manpages
+ for section in {1..8}
+ do
+ local man
+
+ # Call the correct function to write the desired manual page
+ if declare -F "write_man$section" > /dev/null
+ then
+ man=$("write_man$section" "$project")
+ else
+ man=$(write_manX "$section" "$project")
+ fi
+
+ # Make sure there's more than just a NAME heading
+ if (( $(grep -Pc "^.SH" <<< "$man") < 2 ))
+ then
+ [[ -n "$BOCS_VERBOSE" ]] && printf "Warning: Empty manpage %s, ignoring it\n" "$project.$section.man" >&2
+ continue
+ fi
+
+ # Write the manpage to a file
+ cat <<< "$man" > "$project.$section.man"
+
+ unset man
+ done
+
+ # Report on leftover blocks
+ while read -r block
+ do
+ filename="$(basename "$block")"
+
+ [[ $filename =~ ^man.X: ]] && continue
+ printf "Warning: Unused block file %s\n" "$filename" >&2
+ done < <(find "$WORKDIR" -iname "man.*.txt")
+
+ # Clean up
+ rm -fr -- "$WORKDIR"
+}
+
+usage() {
+ cat <<EOF >&2
+Usage:
+ bocs -h
+ bocs [-c <prefix>] [-n <name>] [-t <date>] [-v] <files...>
+
+Options:
+ -c Which prefix or comment style to use when looking for bocs-style docblocks. Defaults to ##. The prefix must always be followed by a space, regardless of its value.
+ -h Show this help message.
+ -n Set the volume name for the manpages.
+ -t Set the date metadata for the manpages. If omitted, the current date will be used.
+ -v Enable verbose mode, logging additional info to STDERR.
+
+Arguments:
+ files One or more files to render the bocs-style docblocks from.
+
+Extract bocs-style docblocks from source code files and renders them
+into manual pages for section 1 through 8.
+EOF
+}
+
+## :bocs:
+## :section: 3
+## :heading: description
+##
+## This manpage exists mostly as an example file to show what a section 3
+## manpage generated by **bocs(1)** looks like.
+
+## :bocs:
+## :section: 3
+## :heading: functions
+## :name: prepare_block_numbered
+##
+## Split the full.txt contents into individual blocks, so they can be easily
+## processed individually.
+prepare_block_numbered() {
+ local blockn=0
+ local blockl=$(( $(grep -Pc "^:bocs:" < "$WORKDIR/full.txt" | wc -c) - 1 ))
+
+ [[ -n "$BOCS_VERBOSE" ]] && printf "Extracting blocks\n" >&2
+
+ while read -r line
+ do
+ if [[ $line == ":bocs:" ]]
+ then
+ blockn=$(( blockn + 1 ))
+ [[ -n "$BOCS_VERBOSE" ]] && printf " %0${blockl}d\n" "$blockn" >&2
+ continue
+ fi
+
+ printf "%s\n" "$line" >> "$WORKDIR/block-$blockn.txt"
+ done < "$WORKDIR/full.txt"
+}
+
+## :bocs:
+## :section: 3
+## :heading: functions
+## :name: prepare_block_named
+## :param: The filename of the (numbered) block to convert into a named variant.
+##
+## Turn the filenames of numbered blocks into named blocks. This assists in
+## disambiguating easily for later processing, while also ensuring subheadings
+## get ordered alphabetically.
+prepare_block_named() {
+ local block="$1" ; shift
+ local blockn
+ local name
+ local section
+ local heading
+
+ # Extract metadata
+ blockn="$(grep -o "[[:digit:]]" <<< "$block" | tail -n 1)"
+ heading="$(grep -Po "(?<=:heading:) .*" < "$block" | awk '{$1=$1};1')"
+ name="$(grep -Po "(?<=:name:) .*" < "$block" | awk '{$1=$1};1')"
+ section="$(grep -Po "(?<=:section:) .*" < "$block" | awk '{$1=$1};1')"
+
+ # Make sure everything is set appropriately
+ [[ -z "$section" ]] && section=3
+ [[ -z "$heading" ]] && heading=undef
+
+ # Rename file
+ if [[ -n "$name" ]]
+ then
+ mv -- "$block" "$WORKDIR/man.$section:$heading:$name.txt"
+ return
+ fi
+
+ mv -- "$block" "$WORKDIR/man.$section:$heading.txt"
+}
+
+## :bocs:
+## :section: 3
+## :heading: functions
+## :name: prepare_scan
+## :param: The prefix to use to find bocs-style docblocks.
+## :params: One or more paths to include for scanning.
+##
+## Scan all given files for bocs docblocks. This function is _not_ recursive,
+## any directories given will be silently ignored.
+prepare_scan() {
+ local prefix="$1" ; shift
+
+ [[ -n "$BOCS_VERBOSE" ]] && printf "Scanning files\n" >&2
+
+ for file in "$@"
+ do
+ [[ ! -f "$file" ]] && continue
+
+ [[ -n "$BOCS_VERBOSE" ]] && printf " %s\n" "$file" >&2
+ awk '$1 == "'"$prefix"'" { $1=""; print(substr($0, '"${#prefix}"')) }' < "$file" >> "$WORKDIR/full.txt"
+ done
+}
+
+## :bocs:
+## :section: 3
+## :heading: functions
+## :name: write_manX
+## :param: The section number of the manpage to render.
+## :param: The name of the project this manpage is being rendered for.
+## :params: The names of each heading to render.
+##
+## A generic function to render any manpage. If no headings are explicitly
+## given, it will attempt to render the following headings, in this order:
+##
+## - NAME
+## - SYNOPSIS
+## - DESCRIPTION
+## - EXAMPLES
+## - SEE ALSO
+## - AUTHORS
+##
+## Any empty headings will be silently left out of the resulting manpage.
+write_manX() {
+ local section="$1" ; shift
+ local project="$1" ; shift
+
+ # Set defaults for which subsections to use
+ if (( $# < 1 ))
+ then
+ set -- name synopsis description examples "see also" authors
+ fi
+
+ # Write each section in order
+ [[ -n "$BOCS_VERBOSE" ]] && printf "Writing %s\n" "$project.$section.man" >&2
+
+ # Write manpage metadata
+ printf '.TH "%s" "%s" "%s"\n' "$project" "$section" "$date"
+
+ # Write each subsection in order
+ for heading in "$@"
+ do
+ heading_cmd="${heading// /_}"
+
+ if declare -F "write_man${section}_${heading_cmd}" > /dev/null
+ then
+ "write_man${section}_${heading_cmd}" "$project"
+ continue
+ fi
+
+ if declare -F "write_manX_${heading_cmd}" > /dev/null
+ then
+ "write_manX_${heading_cmd}" "$section" "$project"
+ continue
+ fi
+
+ write_manX_ "$heading" "$section" "$project"
+ done
+}
+
+## :bocs:
+## :section: 3
+## :heading: functions
+## :name: write_manX_
+## :param: The name of the heading to render.
+## :param: The section number this block is being rendered for.
+## :param: The name of the project this block is being rendered for.
+##
+## This is a generic means of just rendering some text under a heading. It
+## should be overwritten by more specific functions for various headings to
+## format them in a nicer way.
+write_manX_() {
+ local heading="$1" ; shift
+ local section="$1" ; shift
+ local project="$1" ; shift
+ local block="$WORKDIR/man.$section:$heading.txt"
+
+ if [[ ! -f "$block" ]]
+ then
+ if [[ $section == "X" ]]
+ then
+ return
+ fi
+
+ write_manX_ "$heading" "X" "$project"
+ return
+ fi
+
+ [[ -n "$BOCS_VERBOSE" ]] && printf " %s\n" "${heading^^}" >&2
+
+ printf ".SH %s\n" "${heading^^}"
+
+ [[ -n "$BOCS_VERBOSE" ]] && printf " Including %s\n" "$(basename "$block")" >&2
+
+ # Add the general description
+ grep -v '^:' < "$block" | lowdown -t man
+
+ # Clean up used block
+ if [[ $section != "X" ]]
+ then
+ rm -- "$block"
+ fi
+}
+
+## :bocs:
+## :section: 3
+## :heading: functions
+## :name: write_manX_name
+## :param: ??? TODO
+## :param: The section number to render the NAME heading for.
+##
+## A generalized function to render the NAME heading.
+write_manX_name() {
+ printf ".SH %s\n.PP\n%s\n" "NAME" "$2"
+}
+
+## :bocs:
+## :section: 3
+## :heading: functions
+## :name: write_man3
+## :param: The name of the project this block is being rendered for.
+##
+## This function renders a section 3 manual page. It differs from the
+## generalized **write_manX** function in the possible included subheadings.
+write_man3() {
+ write_manX 3 "$1" \
+ "name" \
+ "description" \
+ "variables" \
+ "functions" \
+ "see also" \
+ "authors"
+}
+
+## :bocs:
+## :section: 3
+## :heading: functions
+## :name: write_man3_functions
+## :param: The name of the project this heading is being rendered for.
+##
+## A section 3 specific function to render the FUNCTIONS heading.
+write_man3_functions() {
+ local project="$1" ; shift
+ local paramn
+
+ if (( $(find "$WORKDIR" -iname "man.3:functions:*.txt" | wc -l) < 1 ))
+ then
+ return
+ fi
+
+ [[ -n "$BOCS_VERBOSE" ]] && printf " FUNCTIONS\n" >&2
+
+ printf ".SH %s\n" "FUNCTIONS"
+
+ for block in "$WORKDIR/man.3:functions:"*".txt"
+ do
+ [[ $block == "$WORKDIR/man.3:functions:*.txt" ]] && continue
+
+ [[ -n "$BOCS_VERBOSE" ]] && printf " Including %s\n" "$(basename "$block")" >&2
+
+ # Create new section
+ printf ".SS %s\n" "$(grep -Po "(?<=:name:) .*" "$block" | awk '{ $1=$1 }; 1')"
+
+ # Handle opts
+ if grep -q '^:opt:' < "$block"
+ then
+ while read -r _ opt description
+ do
+ printf ".I %s\n%s\n.br\n" "$opt" "$description"
+ done < <(grep '^:opt:' < "$block")
+
+ printf ".PP\n"
+ fi
+
+ # Handle params
+ paramn=1
+
+ while read -r tag description
+ do
+ printf ".I $"
+ if [[ $tag == ":params:" ]]
+ then
+ printf "@"
+ else
+ printf "%s" "$paramn"
+ paramn=$(( paramn + 1 ))
+ fi
+
+ printf "\n%s\n.br\n" "$description"
+ done < <(grep '^\(:params\?:\)' < "$block")
+
+ # Add the general description
+ grep -v '^:' < "$block" | lowdown -t man
+
+ # Clean up used block
+ rm -- "$block"
+ done
+
+}
+
+## :bocs:
+## :section: 3
+## :heading: functions
+## :name: write_man3_variables
+## :param: The name of the project this heading is being rendered for.
+##
+## A section 3 specific function to render the VARIABLES heading.
+write_man3_variables() {
+ local project="$1" ; shift
+
+ if (( $(find "$WORKDIR" -iname "man.3:variables:*.txt" | wc -l) < 1 ))
+ then
+ return
+ fi
+
+ [[ -n "$BOCS_VERBOSE" ]] && printf " VARIABLES\n" >&2
+
+ printf ".SH %s\n" "VARIABLES"
+
+ for block in "$WORKDIR/man.3:variables:"*".txt"
+ do
+ [[ $block == "$WORKDIR/man.3:variables:*.txt" ]] && continue
+
+ [[ -n "$BOCS_VERBOSE" ]] && printf " Including %s\n" "$(basename "$block")" >&2
+
+ # Create new section
+ printf ".SS %s\n" "$(grep -Po "(?<=:name:) .*" < "$block" | awk '{ $1=$1 }; 1')"
+
+ # Add the general description
+ grep -v '^:' < "$block" | lowdown -t man
+
+ # Clean up used block
+ rm -- "$block"
+ done
+}
+
+main "$@"