You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

634 lines
17 KiB
Bash

#!/bin/bash
#
# README.World Knowledge Management Utility
#
# Please maintain the style as described in Google's Shell Style Guide
# <https://google.github.io/styleguide/shellguide.html>
#
# Copyright © 2020 Robert Sterling Muhlestein.
#
# 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, version 2.
#
# 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/>.
#######################################
# Assumes modern terminal with RGB.
declare NOCOLOR=
[[ -t 1 ]] || NOCOLOR=y
declare GOLD=$'\033[38;2;184;138;0m'
declare RED=$'\033[38;2;255;0;0m'
declare GREY=$'\033[38;2;100;100;100m'
declare CYAN=$'\033[38;2;0;255;255m'
declare GREEN=$'\033[38;2;0;255;0m'
declare RESET=$'\033[0m'
if [[ -n "$NOCOLOR" ]] ; then
GOLD=
RED=
GREY=
CYAN=
GREEN=
RESET=
fi
#######################################
# Validate that a current knowledge node has been declared as KN
# environment variable.
if [[ -z "${KN}" ]]; then
echo "${RED}Please set \$KN to current knowledge node path."
exit 1
fi
#######################################
# Each command must have an accompanying function that begins with an
# underscore to which the main command will delgate.
readonly -a COMMANDS=(
usage render build preview previewstop mkmanifest checkone audit find
title rss logdir logpath datepath logroot recdir
)
#######################################
# Print the first argument as a colored message and exit with
# error status.
#
# Globals:
# RED
# Arguments:
# Message to print
# Outputs:
# Red message text
# Returns:
# Always returns 1
_fail () {
echo "${RED}$1${RESET}" >&2
exit 1
}
#######################################
# Returns true if first argument is the name of a command in the PATH.
#
# Arguments:
# Message to print.
_have () {
which "$1" &>/dev/null
}
#######################################
# Returns true if process is currently running.
#
# Arguments:
# Process ID number.
_proc () {
kill -0 "$1" 2>/dev/null
}
#######################################
# Skip through a timestamped MANIFEST file to the first matching line
# and continue to output the name/IDs of the lines beginning with that
# line until the end of the file. Will look for a file in
# current directory named MANIFEST if one is not passed as the second
# argument. The format of the file is a float that is seconds since
# epoch (potentially with nanoseconds) followed by whitespace and then
# the name/ID of a knowledge node (directory):
#
# 1591796821.3353818530 boosts/logistics
#
# Arguments:
# Exact string to match a line in the file.
# Path to the file to skip through.
_skipto () {
local there
local match="$1"
local file="${2:-MANIFEST}"
if [[ -z "${match}" ]]; then
read -r changed line <<< $(head -1 ${file})
echo ${line}
return
fi
while read -r changed line; do
[[ "${match}" == "${line}" ]] && there=y
[[ "${there}" != y ]] && continue
echo "${line}"
done < "${file}"
}
#######################################
# Allows the reading of all combined arguments into a string buffer that
# is then echoed, or if no arguments are detected reads fully from
# standard input until there is none left and echoes that. This is
# notable because it allows the usage of heredoc input instead of
# arguments making for much cleaner blocks of text. [Ex: declare
# id=$(_argsorin $*)]
#
# Arguments:
# All are combined into string.
_argsorin () {
local buf="$*" line IFS
if [[ -n "${buf}" ]]; then
echo "${buf}"
return
fi
read line
buf="${line}"
while IFS= read line; do
buf="${buf}\n${line}"
done
echo "${buf}"
}
#######################################
# Create a MANIFEST file in the root of the knowledge base that contains
# the knowledge node identifiers (relative paths) and the last modified
# time in seconds since Unix epoch (first). Anything beginning with
# underscore (_) will be ignored. The MANIFEST file is the only file
# required by subscribers to identify if anything has changed in the
# knowledge base since the last time it was the subscriber cached it
# locally. The root node of the knowledge base is a single dot.
#
# Arguments:
# Path to the manifest file. MANIFEST by default.
_mkmanifest () {
declare line IFS file="${1:-MANIFEST}"
while IFS= read -r line; do
line=${line#/}
line=${line/.\//}
[[ -z "${line}" ]] && continue
echo "${line}"
done <<< $(
find . ! -path '*/_*' -a \
-name 'README.md' \
-printf "%T@ %h\n" \
| sort -d) > "${file}"
}
#######################################
# Output the full path to the current log file for the current day (by
# default). Normal `date` command adjustments can be added as extra
# arguments as they are passed directly to `date -d`.
_logpath () {
local dir=$(_logdir "$*")
echo "${dir}/README.md"
}
#######################################
# Output the full path to the current log dir for the current day (by
# default). Normal `date` command adjustments can be added as extra
# arguments as they are passed directly to `date -d`.
_logdir () {
local relpath=$(_datepath "$*")
echo "${KN}/log/${relpath}"
}
#######################################
# Output the full path to the current log rec dir for the current day
# (by default). This is where video and audio recordings YAML documents
# should normally be kept (but not where the actual audio and
# video files should be saved due to size). Normal `date` command
# adjustments can be added as extra arguments as they are passed
# directly to `date -d`.
_recdir () {
local dir=$(_logdir "$*")
echo "${dir}/rec"
}
#######################################
# Outputs the full path to the root log directory for the current
# knowledge node.
_logroot () {
echo "${KN}/log"
}
#######################################
# Output the current date as a path for use when creating content within
# a directory file structure that models chronological data. Accepts
# anything that can be passed to `date -d`.
_datepath () {
date +%4Y/%m/%d "-d $@"
}
#######################################
# Call the pandoc command to render a single node passed as the
# first and only argument. The HTML index.html output is created in the
# same knowledge node subdirectory. A single dot (.) can be passed to
# render the root node of the knowledge base.
#
# Globals:
# README
# RED
# CYAN
# RESET
# Arguments:
# Node ID
# Outputs:
# Prints colored error output from Pandoc
#
# TODO allow more pandoc arguments to be passed from configuration
_render () {
local error style
local node="${1%/README.md}"
local template='--template=main'
local in="${node}/README.md"
local hi='--no-highlight'
local out="--output=${node}/index.html"
local ddir="${README}/assets"
[[ ! -e "${in}" ]] && _fail "Could not find ${CYAN} '${in}'"
[[ -e "${README}/global.yml" ]] && gdata="global.yml"
[[ -e "${node}/styles.css" ]] && style="--metadata=xstyles:styles.css"
[[ -e "${node}/template.html" ]] && template="--template=${node}/template.html"
error=$(pandoc -s -M title=_ ${nosyn} --data-dir=${ddir} ${hi} ${out} \
${template} ${style} ${gdata} ${in} 2>&1)
[[ -n "${error}" ]] && echo ${CYAN}${node}:${RED} ${error}${RESET} >&2
}
#######################################
# Makes the manifest again and renders all the nodes of the knowledge
# base asyncronously. No effort is made to stagger the rendering of nodes
# into any sort of workgroup since rendering a single node is very fast
# and most operating systems can easily handle concurrent rendering of
# every existing node.
#
# Globals:
# GREY
# RESET
# Arguments:
# None
# Outputs:
# Prints colored summary of number of nodes rendered and speed.
_build () {
local begin end
local -a pids
local -i count
begin=$(date +%s)
_mkmanifest
_render '.'
while read -r change node; do
_render "${node}" &
count+=1
done < ./MANIFEST
wait
end=$(date +%s)
_rss
echo "${GRAY}Rendered ${count} nodes in $((end-begin)) seconds.${RESET}"
}
#######################################
# Checks a single node for broken links and such. Depends on `muffet`
# being installed.
#
# Globals:
# GREEN
# RESET
# Arguments:
# Node name/ID
# Outputs:
# Prints colored 'No broken URLs detected.' if passing
# Prints any error output from muffet if failing
# TODO:
# Read target node as second argument or from config
_checkone () {
_have muffet || _fail 'Install https://github.com/raviqqe/muffet'
[[ -e .previewpid ]] || _fail "Doesn't look like preview is running (no .previewpid)."
declare node="http://localhost:3001/$1"
muffet \
--one-page-only "${node}" \
--exclude 'https://duck.com' \
--exclude 'https://duckduckgo.com' \
--exclude 'https://www.lifewire' \
--exclude 'http://linuxcommand' \
--exclude 'https://vimgenius.com'
if [[ $? != 0 ]]; then
return 1
fi
echo " ${GREEN}No Broken URLs detected.${RESET}"
}
#######################################
# Slowly checks one node at a time from the MANIFEST keeping track of
# each as it goes. This is useful for interactively correcting
# potentially dozens of broken links. To reset to the beginning simple
# remove the .lastchecked file.
#
# Globals:
# GREY
# RESET
# Arguments:
# None
# Outputs:
# Prints 'Checking <node>' and the output of checknode for each
_audit () {
# see _checkone for dependency validations
local last node
if [[ ! -e .lastchecked ]]; then
local line=$(head -1 MANIFEST)
node=${line#* }
echo $node > .lastchecked
return
fi
read -r last < .lastchecked
while read -r node; do
echo "${GREY}Checking ${node}${RESET}"
_checkone "${node}"
local rval=$?
echo "${node}" >| .lastchecked
if [[ ${rval} != 0 ]]; then
return 1
fi
done <<< $(_skipto "${last}")
}
#######################################
# Runs the amazing browser-sync live site previewing utility
# putting it into the background and writing the PID to .previewpid.
# The preview can later be disabled with previewstop. Preview has to
# be running for the check* COMMANDS to work.
#
# Globals:
# GREY
# GREEN
# CYAN
# RESET
# Arguments:
# None
# Outputs:
# Error output if not browser-sync
# Returns:
# Fails if no browser-sync detected on the system
# Success if browser-sync started
_preview () {
_have browser-sync || _fail 'Need to install browser-sync.'
declare pid=$(head -1 .previewpid 2>/dev/null)
if [[ -n "${pid}" ]]; then
if _proc "${pid}"; then
_fail "Already previewing."
else
rm .previewid 2>/dev/null
fi
fi
browser-sync start \
--no-notify --no-ui \
--ignore '**/.*' \
-sw &>/dev/null &
pid=$!
echo "${pid}" >| .previewpid
echo "${GREEN}Previewing with ${CYAN}browser-sync ${GREY}(${pid})${RESET}"
}
#######################################
# Stops the pid contained .previewpid if found.
#
# Globals:
# GREY
# GREEN
# RESET
# Arguments:
# None
# Outputs:
# Error messages on failure
# "Previewing stopped ..." on success
# Returns:
# Failure (1) if nothing being previewed
# Success (0) if preview running and stopped.
_previewstop () {
declare pid=$(head -1 .previewpid 2>/dev/null)
[[ -z "${pid}" ]] && _fail "Don't appear to be previewing (no preview PID found)."
kill "${pid}" && rm .previewpid
echo "${GREEN}Previewing stopped ${GREY}(killed ${pid} and removed .previewpid)${RESET}"
}
#######################################
# Searches the MANIFEST for a matching substring passed as the first and
# only parameter and prints the resulting full node identifiers with the
# substring highlighted in green. Also searches the _redirects list
# for possible shortcuts and other former identifiers
#
# Globals:
# GREY
# GREEN
# RESET
# Arguments:
# Substring to look for.
# Outputs:
# Prints the node names/IDs of matches.
# Prints errors if nothing to find.
# Returns:
# Failure (1) if nothing to find.
# Success (0) if either something found or not found.
# TODO:
# Replace with other find that matches based on header and tag
# hits with priority and puts the top 20 in a select list
_find () {
declare substr="$1"
[[ -z "$1" ]] && _fail 'Nothing to find.'
while read -r changed name; do
[[ -z "${name}" ]] && continue
echo "${GREY}$(_url)/${name%%${substr}*}${GREEN}${substr}${GREY}${name##*${substr}}"
done <<< $(grep "${substr}" MANIFEST)
while read -r redirect; do
[[ -z "${name}" ]] && continue
echo "${GREY}$(_url)/${redirect%%${substr}*}${GREEN}${substr}${GREY}${redirect##*${substr}}"
done <<< $(grep "${substr}" _redirects)
}
#######################################
# Returns the URL YAML field from the main README.md file.
_url () {
local line=$(grep ^URL: README.md)
echo ${line#URL:}
}
#######################################
# Returns the Short YAML field from the main README.md file.
_short () {
local line=$(grep ^Short: README.md)
echo ${line#Short:}
}
#######################################
# Returns the Description YAML field from the main README.md file.
_description () {
local line=$(grep ^Description: README.md)
echo ${line#Description:}
}
#######################################
# Returns the copyright YAML field from the main README.md file.
_copyright () {
local line=$(grep ^Copyright: README.md)
echo ${line#Copyright:}
}
#######################################
# Parses out the Title from the specified node and prints it.
#
# Globals:
# GREEN
# RESET
# Arguments:
# Node name/ID
# Outputs:
# Prints the title of the given node in color.
_title () {
declare -a nodes
if [[ -n "$1" ]]; then
nodes="$@"
else
while IFS= read -r line; do
nodes+=(${line})
done
fi
for node in ${nodes[@]}; do
while read -r line; do
if [[ "$line" =~ ^Title:\ ]]; then
echo "${GREEN}${line:7}${RESET}"
fi
done < "${node}/README.md" 2>/dev/null
done
}
#######################################
# Creates an RSS feed if the node is a log.
_rss () {
declare dir=${1:-.}
declare file="${dir}"/README.md
egrep '^## +(Mon|Tue|Wed|Thu|Fri|Sat|Sun)' "${file}" &>/dev/null
if [[ $? == 0 ]]; then
# TODO make all of this only add if detected
echo '<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<atom:link href="'$(_url)'/rss.xml" rel="self" type="application/rss+xml" />
<title>'$(_short)'</title>
<link>'$(_url)'</link>
<description>'$(_description)'</description>
<copyright>'$(_copyright)'</copyright>' > "${dir}"/rss.xml
declare -i item=0
buf=""
while read line; do
if [[ "${line}" =~ ^##\ +(Mon|Tue|Wed|Thu|Fri|Sat|Sun) ]]; then
if [[ $item > 0 ]]; then
echo "${buf}" |perl -pe 's,\[(.+)\](.*),\1,;s,(<|>),,g;s,&,&amp;,g' >> "${dir}"/rss.xml
echo '</description></item>' >> "${dir}"/rss.xml
buf=""
fi
item=$((item+1))
echo '<item>' >> "${dir}"/rss.xml
#guid=${line#\#\# *}
#guid=${guid// }
#TODO user relative link creator for URI
echo '<title>'${line#\#\# *}'</title>' >> "${dir}"/rss.xml
#echo '<guid>'$guid'</guid>' >> "${dir}"/rss.xml
echo '<description>' >> "${dir}"/rss.xml
continue
fi
if [[ "${line}" == "" ]]; then
continue
fi
buf="$buf $line"
done < <(egrep '^## +(Mon|Tue|Wed|Thu|Fri|Sat|Sun)' -A 5 "${file}")
echo "${buf}" |perl -pe 's,\[(.+)\](.*),\1,;s,(<|>),,g;s,&,&amp;,g' >> "${dir}"/rss.xml
echo '</description></item>' >> "${dir}"/rss.xml
echo '</channel></rss>' >> "${dir}"/rss.xml
return 0
else
echo ${RED}Not a log file.${RESET}
return 1
fi
}
#######################################
# Prints each of the command names and exits.
_usage () {
echo "${GREY}usage: ${GOLD}kn ${CYAN}<cmd>${RESET}"
for i in ${COMMANDS[@]}; do echo " ${CYAN}$i"; done
}
#######################################
# Checks if command is being used in tab completion context (`complete -C
# kn kn` and COMP_LINE set). If so handle completion and exit.
# Otherwise, assume execution context. Detect the README working
# directory since more than one README knowledge base may exist on
# a single system. Look for full COMMAND matches for the first argument.
# Then accept the first matching substring as the command to use. Then
# delegate.
main () {
local c cmd="${1-usage}"
shift 2>/dev/null
# knowledge base detection
#[[ -d .git && -e README.md ]] && export README="${PWD}"
[[ -e README.md ]] && export README="${PWD}"
[[ -z "${README}" ]] && _fail "Need to set README environment variable"
cd "${README}" || _fail "Could not change into README directory: ${README}"
# tab completion
if [[ -n "$COMP_LINE" ]]; then
for c in ${COMMANDS[@]}; do
[[ "${c:0:${#1}}" == "$1" ]] && echo "${c}"
done
exit 0
fi
# full command match
for c in ${COMMANDS[@]}; do
if [[ "${c}" == "${cmd}" ]]; then
"_${c}" "$@"
exit 0
fi
done
# partial command match
for c in ${COMMANDS[@]}; do
if [[ "${c}" =~ ^${cmd} ]]; then
"_${c}" "$@"
exit 0
fi
done
}
# NORUN allows this file to be sourced while checking for syntax errors
# and unit testing specific functions.
[[ -z "${NORUN}" ]] && main "$@"