mirror of
https://github.com/rwxrob/dot
synced 2024-11-16 21:25:29 +00:00
633 lines
17 KiB
Bash
Executable File
633 lines
17 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# Knowledge Management Utility
|
|
#
|
|
# Please maintain the style as described in Google's Shell Style Guide
|
|
#
|
|
# 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,&,&,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,&,&,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 "$@"
|