#!/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 . ####################################### # 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 ' 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 ' '$(_short)' '$(_url)' '$(_description)' '$(_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 '' >> "${dir}"/rss.xml buf="" fi item=$((item+1)) echo '' >> "${dir}"/rss.xml #guid=${line#\#\# *} #guid=${guid// } #TODO user relative link creator for URI echo ''${line#\#\# *}'' >> "${dir}"/rss.xml #echo ''$guid'' >> "${dir}"/rss.xml echo '' >> "${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 '' >> "${dir}"/rss.xml echo '' >> "${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}${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 "$@"