diff options
| author | Patrick Spek <p.spek@tyil.nl> | 2024-08-12 13:03:50 +0200 |
|---|---|---|
| committer | Patrick Spek <p.spek@tyil.nl> | 2024-08-12 13:03:50 +0200 |
| commit | e91a4e7cc9646de16ac955321473d0e1237b4b60 (patch) | |
| tree | 01b9630c57c1f9c31781b130ebc59b8b5f90a0b5 | |
| download | bocs-e91a4e7cc9646de16ac955321473d0e1237b4b60.tar.gz bocs-e91a4e7cc9646de16ac955321473d0e1237b4b60.tar.bz2 | |
| -rwxr-xr-x | bocs.bash | 487 |
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 "$@" |
