Auditing the KBM Wiki for Missing Titles: Part 2, The Audit Process

In Part 1 I discussed the get_urls.sh script that generated lists of all KBM Wiki URLs, namespace (group) by namespace. In this thread I'll describe how I took those lists of URLs and generated Markdown tables, one for each namespace, with hotlinks to each Wiki page and identifying the presumed status of the title for each page: Missing, Error, or OK. The Missing and Error entries also have a suggested title, automatically generated, ready to paste in.

The goal is to have a simple way of identifying what needs to be done where and to be able to make fixes in a minimum number of steps. This post/thread describes how I did that.

The next post, Part 3, will provide the results in the form of editable tables for each of the KBM Wiki namespaces, instructions for using them, and so on. It will be the place for any discussion of how to handle pages that don't fit the basic naming pattern.

You can help with updating the Wiki by joining the discussion in Part 3 and working with the Audit tables there. You don't need to understand either the previous script, get_urls.sh or the script described here, audit.sh, at all to be able to help with editing the Wiki (assuming you have permission to edit the Wiki).

The audit.sh script performs these steps:

  1. Determine the input type, command argument or STDIN (pipe).
  2. Output a markdown table header with three columns.
  3. Loop through the input, one line at a time, doing the following:
  4. Print a "." to the Terminal for each file processed so I can can see that it is doing something when working on long lists.
  5. Download the page HTML file with curl and save the text in a variable.
  6. Do a few validity check like successfully downloading a file and the file containing a Wiki body section.
  7. Extract the page body with sed.
  8. Identify the group/namespace name and the file name from the URL input line.
  9. Build the expected above names.
  10. Extract the actual <h1> title from the downloaded page.
  11. Compare the above two values.
  12. If they match, the status is OK.
  13. If they don't match report that either the actual title is missing or that there is a difference (Error) between the actual and expected titles.
  14. In either the Missing or Error status, convert the expected title to the format used in the Wiki Editor.
  15. Output the URL, the Status (including the Error title), and the suggested new title in a Markdown table entry.

Because I created the script to be able to handle either an input file or a list coming from STDIN, how the script is called determines where the output goes. Unlike the first script, I didn't specify an output file, instead I called the script with a series of commands like:

$ ./audit.sh urls_collection.txt > audit_collection.md

There are 13 namespaces/groups. I ran the get_urls.sh once for each of those groups/namespaces and got 13 text files with lists of the corresponding Wiki page URLs. Running 13 commands like the one above got me 13 Markdown table files.

Heres the audit.sh script, followed by the audit_collection.md markdown table for the collection namespace as an example of the output.

The `audit.sh` script takes a list of URLS for Wiki pages and checks the status of whether or not it has a proper title ( expand / collapse )
#!/usr/bin/env bash

# #######################################################################
#
# Keyboard Maestro Wiki Title Audit Script
#
# Reads a list of wiki page URLs (one per line)
#  from either stdin or a given filename and reports a status for each page:
#   - a correct H1 title:           OK
#   - no title at all:              MISSING
#   - a title with incorrect text:  ERR, show found value
#
# And prints a table with three columns:
#   - a link to the file
#   - the above status
#   - for MISSING or ERR, a suggested title in Wiki Editor format
#       derived from the filename and the namespace name in the URL.
#
# Output format: Markdown table
#
# #######################################################################

# ---------------------------------------------------
# Notify of start because it can take a while seeming to not be working.
# Use printf to not add a newline so we can add continuous progress dots,
# one for each URL in the inner loop.
#
# printf    :Use printf utility,
# │      "$0 started: "    :with this formating template (no variables).
# │      │              >&2    :Send output to STDOUT
# │      │              │
  printf "$0 started: " >&2

# ---------------------------------------------------
#
# Set Format Option
#
# The $unicode variable determines whether or not to use Unicode characters
# in the output. If not, use the ASCII pipe symbol. If so (true), use the
# U+2223 short vertical line from the math characters for the pipe in tokens
# and the U+2502 vertical line from the box-drawing characters for the
# vertical connecting lines between tokens and command line elements.
#
  unicode=true

# #######################################################################
#
# Verify input
#

# ---------------------------------------------------
# Allow input to be a filename argument or STDIN
# (Using BASH parameter expansion syntax)
#
# input="    :Set value of $input variable to result
#      │ ${    :Begin parameter expansion
#      │ │ 1:    :Look at parameter $1
#      │ │ │ -    :If $1 does not exist then
#      │ │ │ │/dev/stdin    :Use STDIN as default $input
#      │ │ │ ││         }    :end parameter expansion
#      │ │ │ ││         │"    :End of quoted parameter string
#      │ │ │ ││         ││
  input="${1:-/dev/stdin}"

# ---------------------------------------------------
# Check input readability
#
#   If input is not STDIN (i.e. it's a filename) AND it's not readable
#   then print error message to STDERR and exit.
#
# if    :Test the following condition to see if we have bad input
# │  [[    :Built-in BASH Shell Delimeter for Conditional Expression
# │  │       :(Similar to "test" or "[...]" but faster and safer)
# │  │   $input    :If $input value
# │  │   │       !=    :is not equal to
# │  │   │       │   /dev/stdin    :STDIN value
# │  │   │       │   │           &&    :AND
# │  │   │       │   │           │  !    :the next Arg is NOT
# │  │   │       │   │           │  │ -r    :readable
# │  │   │       │   │           │  │  │  $input    :Arg is $input
# │  │   │       │   │           │  │  │  │       ]]    :End of [[ ... ]]
# │  │   │       │   │           │  │  │  │       │  ;    :End of if test
# │  │   │       │   │           │  │  │  │       │  │ then    :Begin success
# │  │   │       │   │           │  │  │  │       │  │ │         :branch
# │  │   │       │   │           │  │  │  │       │  │ │
  if [[ "$input" != "/dev/stdin" && ! -r "$input" ]] ; then

    # echo    :Output text message
    # │    "Error: Cannot read input file:    :First part of error message
    # │    │                               $input    :Name of input file
    # │    │                               │     "    :End of output string
    # │    │                               │     │ >&2    :Send output to STDERR
    # │    │                               │     │ │
      echo "Error: Cannot read input file: $input" >&2

    # exit    :Quit the whole script on this error.
    # │    1    :Status code for exit, 1 = error
    # │    │
      exit 1

  fi # End-if [[ ... ]]


# #######################################################################
#
# Output a Markdown table header in prep for following markdown table rows.
# There are no input parameters here, so it just prints what is given,
# including a newline at the end.
#
# [This layout uses box draw ^Vu2502 for │ and math ^Vu2223 for ∣ to
# distinguish pipe symbols from connector lines. u2223 is NOT a true pipe.]

# printf    :Use printf to generate table header
# │      '    : Begin printf format spec
# │      │∣    :| Markdown table column separator
# │      ││ URL    :Column 1 caption
# │      ││ │   ∣    :| Markdown table column separator
# │      ││ │   │ Title Status    :Column 2 caption
# │      ││ │   │ │            ∣    :| Markdown table column separator
# │      ││ │   │ │            │ Notes, Paste-in Title    :Column 3 caption
# │      ││ │   │ │            │ │                     ∣    :| Markdown column
# │      ││ │   │ │            │ │                     │\n    :format code for
# │      ││ │   │ │            │ │                     ││        :newline
# │      ││ │   │ │            │ │                     ││ '    :End format spec
# │      ││ │   │ │            │ │                     ││ │
  printf '| URL | Title Status | Notes, Paste-in Title |\n'
  printf '|-----|--------------|-----------------------|\n'


# #######################################################################
#
# Main input URL loop
#


# ---------------------------------------------------
# Read input line by line as repeating instances of the $url variable
#
# while    :Start loop; continue while following command succeeds
# │     IFS=    :Temporarily set Input Field Separator to empty
# │     │          (prevent trimming/splitting)
# │     │    read    :Read one line from stdin
# │     │    │    -r    :Raw mode; do not treat backslashes as escapes
# │     │    │    │  url    :Store the line in variable "url"
# │     │    │    │  │  ;    :End condition command.
# │     │    │    │  │  │ do    :Begin loop body (ends at "done").
# │     │    │    │  │  │ │
  while IFS= read -r url; do

  # -------------------------------------------------
  # Skip empty lines
  #
  # Note, this could be constructed similarly to other tests, using
  # if ...; then ... fi. In this case, there is only one command on
  # one branch of the potential "if", and that is the "continue".
  # That allows us to use the "&&" construction. In this case, the
  # command after the && will only be evaluated if the first test
  # succeeds. Here "success" means the $url veriable length is zero.
  #
  # [[   :Begin Bash conditional test
  # │  -z    :Test if string length is zero
  # │   │ "$url"    :Where string is the variable $url 
  # │   │  │     ]]    :End conditional test
  # │   │  │     │  &&    :If previous test succeeded ($url length = 0)
  # │   │  │     │  │  continue    :Skip to next loop iteration
  # │   │  │     │  │  │
    [[ -z "$url" ]] && continue

  # If we're here, the above test failed, the $url is non-empty,
  # so proceed with the rest of this process.
  # First, add a progress dot to the ongoing STDERR notification,
  # with no newline at the end.
  #
  # printf    :Print using the following format spec.
  # │      ". "    :Ouput a string of a period and a space.
  # │      │    >&2    :Send the output to STDERR
  # │      │    │
    printf ". " >&2

  # -------------------------------------------------
  # Fetch HTML

  # get HTML page silently and follow redirects
  #
  # html=    :Assign to variable $html
  # │    $(    :the results of this command
  # │    │ curl    :Run curl program
  # │    │ │    -f    :Fail on HTTP errors
  # │    │ │    │-s    :Silent (no progress output)
  # │    │ │    │ -L    :Follow redirects
  # │    │ │    │ ││ "$url"    :Fetch the URL stored in variable url 
  # │    │ │    │ ││ │     )    :End command to generate var value
  # │    │ │    │ ││ │     │
    html=$(curl -fsL "$url")

  # -------------------------------------------------
  # If there is no page for this URL, add an error message 
  # to the markdown output table.
  #
  # if    :Test the following condition to see if we have data in $html.
  # │  [[    :Built-in BASH Shell Delimeter for Conditional Expression
  # │  │  -z    :Test if string length is zero
  # │  │  │  "$html"    :Where string is the variable $html 
  # │  │  │  │       ]]    :End of [[ ... ]]
  # │  │  │  │       │ ;    :End of if test
  # │  │  │  │       │ │ then    :Begin success branch (string length 0)
  # │  │  │  │       │ │ │
    if [[ -z "$html" ]]; then

    # printf    :If $html is empty, use printf to generate an error line.
    # │      '    :Format output with this templete
    # │      │∣    :markdown column delimiter
    # │      ││ %s    :the first variable ($url) formatted as a string 
    # │      ││ │  ∣    :markdown column delimter
    # │      ││ │  │ ERROR    :Status field content
    # │      ││ │  │ │     ∣    :markdown column delimiter
    # │      ││ │  │ │     │ curl failed to get page    :Notes content
    # │      ││ │  │ │     │ │                       ∣    :md col delim
    # │      ││ │  │ │     │ │                       │\n'    :end of line and
    # │      ││ │  │ │     │ │                       ││         end of format
    # │      ││ │  │ │     │ │                       ││   "$url"    :var for %s
    # │      ││ │  │ │     │ │                       ││   │           in col 1.`
    # │      ││ │  │ │     │ │                       ││   │
      printf '| %s | ERROR | curl failed to get page |\n' "$url"

    continue
  fi


  # #####################################################################
  #
  # Sanity check for non-standard pages
  #

  # Make sure that the MediaWiki body delimiter we need is present.
  # If not, skip this loop and go to next URL.
  #
  # if    :if it is...
  # │  !    :not true that there's a successful result when...
  # │  │ printf    :sending via printf...
  # │  │ │      "%s"    :formatted as a string...
  # │  │ │      │    "$html"    :the contents of the $html variable...
  # │  │ │      │    │       ∣    :piped through...
  # │  │ │      │    │       │ grep    :the grep command...
  # │  │ │      │    │       │ │    -q    :with the Quiet option
  # │  │ │      │    │       │ │    │  '<!-- wikipage start -->'    :looking for the
  # │  │ │      │    │       │ │    │  │                       │     Wiki body marker
  # │  │ │      │    │       │ │    │  │                       │;    :(End of if test)
  # │  │ │      │    │       │ │    │  │                       ││ then    :do this
  # │  │ │      │    │       │ │    │  │                       ││ │
    if ! printf "%s" "$html" | grep -q '<!-- wikipage start -->'; then

  #   printf    :Format the output
  #   │      '    :with this format specification:
  #   │      │∣    :A markdown column separator,
  #   │      ││ %s    :The input variable formated as a string,
  #   │      ││ │  ∣    :a markdown column separator,
  #   │      ││ │  │ SKIPPED    :the Status value for this file,
  #   │      ││ │  │ │       ∣    :ads markdown column separator,
  #   │      ││ │  │ │       │ No wikipage markers found    :the Notes for this file,
  #   │      ││ │  │ │       │ │                         ∣    :a md col separator,
  #   │      ││ │  │ │       │ │                         │\n    :a newline.
  #   │      ││ │  │ │       │ │                         ││ '    :(end of printf spec)
  #   │      ││ │  │ │       │ │                         ││ │ "$url"    :The $url var
  #   │      ││ │  │ │       │ │                         ││ │ │
      printf '| %s | SKIPPED | No wikipage markers found |\n' "$url"

  #   continue    :And then don't do anything else, skip to the next input line.
  #   │
      continue

  # fi    :End of "if", there is no "else".
  # │
    fi


  # #####################################################################
  #
  # Extract wiki page body (KBM-specific)
  #

  # Extract page body between delimiters, skipping header and footer.
  #
  # body=    :Define $body to be the result of the following command.
  # │    $(    :Beginning of command-value definition
  # │    │
    body=$(
    # printf    :Use printf to add \n to $html`.
    # │      "    :Begin printf format
    # │      │%s    :Given variable ($html) will be used as a string
    # │      ││ \n    :Add a newline
    # │      ││ │ "    :End of format sec
    # │      ││ │ │ "$html"    :The variable to format with printf
    # │      ││ │ │ │       ∣    :Pipe output to the next command (sed).
    # │      ││ │ │ │       │
      printf "%s\n" "$html" |

          #  Use sed to extact body text using explicit markers in file;
          #
          # -n    :Do not print anything unless specifically told to.
          #  │  /<...    :From any line containg the "start" marker
          #  │  │                         /<!...    :To any line with the
          #  │  │                         │             "stop" marker
          #  │  │                         │                       p    :Print
          #  │  │                         │                       │
        sed -n '/<!-- wikipage start -->/,/<!-- wikipage stop -->/p'
    )


  # #####################################################################
  #
  # Extract filename and group from URL
  # to construct title for validation.
  #
  # group=    :Define $group variable to be
  # │     $(    :Begin commands to generate variable value
  # │     │
    group=$(

    # printf    :Use printf utility to format the $url
    # │      '%s'    :the format spec, argument formated as a string
    # │      │    "$url"    :Argument, the $url variable
    # │      │    │      ∣    :Send output to next command (sed ...)
    # │      │    │      │ 
      printf '%s' "$url" |

    # Use sed to extact the next to last field of the URL
    #    
    # sed    :Invoke sed utility
    # │   -E    : Use Extended regular expressions
    # │    │ '    :Delimit sed commands string with ' so everything is literal
    # │    │ │s    :Substitute
    # │    │ ││:    :Using : as delimiter to avoid repeating \/ in pattern
    # │    │ │││.*    :Anything (implied from the beginning of the string)
    # │    │ ││││ /    :followed by a literal '/' (: is the delimiter)
    # │    │ ││││ │(    :Begin capture group 1
    # │    │ ││││ ││[^/]    :Anything not a /
    # │    │ ││││ │││   +    :One or more times
    # │    │ ││││ │││   │)    :End capture group 1
    # │    │ ││││ │││   ││/    :Followed by a literal /
    # │    │ ││││ │││   │││[^/]    :Anything not a /
    # │    │ ││││ │││   ││││   +    :One or more times
    # │    │ ││││ │││   ││││   │$    :To the end of the line
    # │    │ ││││ │││   ││││   ││:    :End of match, begin replacement
    # │    │ ││││ │││   ││││   │││\1    :Replace with capture group 1
    # │    │ ││││ │││   ││││   ││││ :    :End of replacement
    # │    │ ││││ │││   ││││   ││││ │'    :End of sed command string
    # │    │ ││││ │││   ││││   ││││ ││
      sed -E 's:.*/([^/]+)/[^/]+$:\1:'

    ) # End group=$( ... )

    filename=$(
      printf '%s' "$url" |

    # Use sed to extact the last field of the URL
    #    
    #      Substitute
    #      ││Using : as delimiter to avoid having to use \/ in match
    #      ││ Anything, as much as possible, up to and including ...
    #      ││ │a / character
    #      ││ ││ And replace that with nothing
    #      ││ ││ │
      sed 's:.*/::'
    ) # End filename=
  

  # #####################################################################
  #
  # Build expected H1 title:
  #   Convert filename underscores to spaces
  #   Capitalize group name
  #   Assemble expected H1 title
  #

  # ---------------------------------------------------
  # Convert filename underscores to spaces using BASH parameter expansion.
  #     Note: The comments explicity use the "␠" U+2420 symbol for space.
  # 
  # name_space    :Define the $name_space variable
  # │          =    :Set value of $name_space variable to the results of
  # │          │${    :parameter expansion, begin here
  # │          ││ filename    :operate on this variable
  # │          ││ │       //    :replace all matches
  # │          ││ │       │ _    :match an underscore
  # │          ││ │       │ │/    :separator
  # │          ││ │       │ ││␠    :replacement: space character
  # │          ││ │       │ │││}    :end parameter expansion, end definition
  # │          ││ │       │ ││││
    name_space=${filename//_/ }

  # ---------------------------------------------------
  # Capitalize group name
  # Use tr to translate lowercase to uppercase reading from the Here-string,
  # for the first character (0) of the $group variable, followed by the
  # second (1) and remaining characters (rest of the string).
  group_title="$(tr '[:lower:]' '[:upper:]' <<< ${group:0:1})${group:1}"

  # Assemble expected H1 title
  expected_h1="$name_space $group_title"

  # ---------------------------------------------------
  # Extract H1 text (if present) from page $body as $raw_h1_text

  raw_h1_text=$(
    printf "%s" "$body" |

    # grep -i    :use grep utility, ignore case (finds <H1 and <h1)
    #       │ '<h1    :search for "<h1" followed by
    #       │  │  [ >]    :either a " " or ">"
    #       │  │   │  '    :End of search string
    #       │  │   │  │ │    :│ pipe symbol, output to next command
    #       │  │   │  │ │
      grep -i '<h1[ >]' |

    #     give only the first line of the results to the next command
      head -n 1 |

    #      Use sed to extact text of <h1> tag from the line:
    #    
    # sed    :Stream EDitor utility
    # │   -E    :Use Extended regular expressions
    # │    │ 's    :Substitute for...
    # │    │  │ .*<h1    :anything (from beginning of line) followed by <h1
    # │    │  │  │   [^>]    :and followed by anything not a >
    # │    │  │  │    │  +    :one or more times
    # │    │  │  │    │  │>    :followed by the closing > of the <h1 tag
    # │    │  │  │    │  ││(    :Begin capture group 1, consisting of...
    # │    │  │  │    │  │││[^>]    :Anything that is not a <,
    # │    │  │  │    │  │││ │  +    :one or more times
    # │    │  │  │    │  │││ │  │)    :End capture group 1
    # │    │  │  │    │  │││ │  ││<\/h1>    :the close of the h1 tag with a literal /
    # │    │  │  │    │  │││ │  │││     .*    :anything at all (to end of line)
    # │    │  │  │    │  │││ │  │││     │ /    :replace match with
    # │    │  │  │    │  │││ │  │││     │ │\1    :capture group 1
    # │    │  │  │    │  │││ │  │││     │ │ │/'    :End of substitution
    # │    │  │  │    │  │││ │  │││     │ │ │
      sed -E 's/.*<h1[^>]*>([^<]+)<\/h1>.*/\1/'

  ) # End of raw_h1_text=$(

  # ---------------------------------------------------
  # Clean up $raw_h1_text as $h1_text by using printf to ensure it'a a string
  # and sending that through sed to strip leading and trailing spaces.
  # 

  # h1_text=    :Set the $h1_text variable to the result of...
  # │       $(    :Begin the defintion of the the value
  # │       │
    h1_text=$(

    # printf    :Format the value using the printf utility
    # │      '    :Begin printf format spec
    # │      │%s'    :A string, and the end of the spec
    # │      ││   "$raw_h1_text"    :The string of the variable
    # │      ││   │              ∣    :| pipe symbol, output to next command
    # │      ││   │              │
      printf '%s' "$raw_h1_text" |

    #      Use sed to remove any spaces at beginning or end of text:
    #    
    # sed '    :Stream EDitor utility, followed by sed command argument
    # │    s/    :Substitute for....
    # │    │ ^    :Starting at the beginning of the line
    # │    │ │[    :The specific character(s)
    # │    │ ││[:space:]    :the POSIX-defined match for space, tab, etc.
    # │    │ │││        ]    :End of search set
    # │    │ │││        │*    :Any number of them
    # │    │ │││        ││//    :Replace by nothing.
    # │    │ │││        ││ │ ;    :SED multi-command separator
    # │    │ │││        ││ │ │ s/   :Substitue for
    # │    │ │││        ││ │ │ │ [[:space:]]*    :Any number of space-type characters
    # │    │ │││        ││ │ │ │ │           $    :Anchored at the end of the line
    # │    │ │││        ││ │ │ │ │           │//    :Replace by nothing. 
    # │    │ │││        ││ │ │ │ │           │ │'    :End of SED commands
    # │    │ │││        ││ │ │ │ │           │ ││
      sed 's/^[[:space:]]*// ; s/[[:space:]]*$//'

  ) # End of h1_text=

  # ---------------------------------------------------
  # Name Validity Check can be done now that both $expected_h1
  # and h1_text variables are defined.

# Print the two values to STDERR for debugging:
# printf 'DEBUG     h1_text=[%q]\n' "$h1_text" >&2
# printf 'DEBUG expected_h1=[%q]\n' "$expected_h1" >&2

  # Create output strings for report
  status=""
  notes=""

  # ---------- Status Logic ----------
  # Check for match first (most common case), then diagnose failure

  if [[ "$h1_text" == "$expected_h1" ]]; then
      status="OK"                      # OK
      notes=":ballot_box_with_check:"  # OK, status is a simple symbol

  elif [[ -z "$h1_text" ]]; then
      status="MISSING"                    # MISSING
      notes="====== $expected_h1 ======"  # Suggested text, formatted
                                          # to paste into Wiki editor
  else
      status="ERR: $h1_text"              # ERR: Show text found
      notes="====== $expected_h1 ======"  # Suggested text, formatted
                                          # to paste into Wiki editor
  fi

  # ---------------------------------------------------
  # Just in case $notes might ever contain a | sympbol, e.g. in any
  # new text, which would break the Markdown table ...
  # Use BASH string substitution to edit. (ChatGPT suggestion)
  #
  # notes=    :set value of $notes variable
  # │     ${    :begin parameter expansion
  # │     │ notes    :operate on this variable
  # │     │ │    //    :replace all matches
  # │     │ │    │ │    :match a literal pipe character
  # │     │ │    │ │/    :separator
  # │     │ │    │ ││\\    :replacement: literal backslash +
  # │     │ │    │ │││ │    :replacement: pipe
  # │     │ │    │ │││  }    :end parameter expansion
  # │     │ │    │ │││  │
    notes=${notes//|/\\|}
  

  # #####################################################################
  #
  # Output a markdown table row
  #
  # printf    :Use printf to print a formatted line, according to the spec.
  # │      '    :Begin printf format spec
  # │      │∣    :| is Markdown table column separator
  # │      ││ [    :Markdown link syntax
  # │      ││ │%s    :Display text will be Arg 1, $group/$filename
  # │      ││ ││ ](    :Markdown link syntax
  # │      ││ ││ │ %s    :Arg 2, $url
  # │      ││ ││ │ │ )    :Markdown link syntax
  # │      ││ ││ │ │ │ ∣    :| is Markdown table column separator
  # │      ││ ││ │ │ │ │ %s    :Arg3, $status
  # │      ││ ││ │ │ │ │ │  ∣    :| is Markdown table column separator
  # │      ││ ││ │ │ │ │ │  │ %s    :Arg4, $notes
  # │      ││ ││ │ │ │ │ │  │ │  ∣    :| is Markdown table column separator
  # │      ││ ││ │ │ │ │ │  │ │  │\n    :newline
  # │      ││ ││ │ │ │ │ │  │ │  ││ '    :End format spec
  # │      ││ ││ │ │ │ │ │  │ │  ││ │ \    :Followed on next line by ...
  # │      ││ ││ │ │ │ │ │  │ │  ││ │ │     ... Four string parameters
  # │      ││ ││ │ │ │ │ │  │ │  ││ │ │
    printf '| [%s](%s) | %s | %s |\n' \
    "$group/$filename" "$url" "$status" "$notes"


# #######################################################################
#
# End of Main input loop
#
# done    :
# │    <    :
# │    │ "$input"    :
# │    │ │
  done < "$input"    # Reminder: $input can be a file or STDIN

# Print completion message to STDERR
# printf    :
# │      "    :
# │      │done.    :
# │      ││    \n    :
# │      ││    │ "    :
# │      ││    │ │ >&2    :
# │      ││    │ │ │
  printf "done.\n" >&2

The table has three columns: URL, Status, and Notes. The URL is a hotlink to the page in the KBM WIki. The Status is one of Missisng, Error, or OK. "Missing" means there's no title at all, "Error" means that a title exists but does not match the standard pattern, and "OK" means the existing title does match the pattern. Notes has a simple check mark when the title is OK and for the others, it offers a suggested title in the format used in the Wiki Editor, ready to copy and paste.

In this collection of KBM "Collection" types, only one WIki page is OK while most are missing titles. Of the Error pages, the "JSON Keys" collection looks like it mighty be improved by changing to match the standard, editing the title to the suggested "JSON Keys Collection". The other two pages with an Error status look like they will take a more editorial judgement call of whethe the standard title is more useful than the explanitory title that is there.

The `audit_collection.md` output file contains the status table for the Wiki `collection` namespace ( expand / collapse )
URL Title Status Notes, Paste-in Title
collection/Clipboard_History MISSING ====== Clipboard History Collection ======
collection/Dictionaries MISSING ====== Dictionaries Collection ======
collection/Dictionary_Keys MISSING ====== Dictionary Keys Collection ======
collection/Finders_Selection ERR: Working with the Finder Selection ====== Finders Selection Collection ======
collection/Folder_Contents MISSING ====== Folder Contents Collection ======
collection/Found_Images OK :ballot_box_with_check:
collection/JSON_Keys ERR: JSON Keys ====== JSON Keys Collection ======
collection/Lines_In MISSING ====== Lines In Collection ======
collection/Mounted_Volumes MISSING ====== Mounted Volumes Collection ======
collection/Number_Range MISSING ====== Number Range Collection ======
collection/Running_Applications MISSING ====== Running Applications Collection ======
collection/Substrings_In ERR: For Each Substrings In RegEx Match ====== Substrings In Collection ======
collection/Variables MISSING ====== Variables Collection ======