Pandoc would output warnings about a lack of title, although we are not using it in the template. Also, the tool would fail to do some git commands because it was checking the wrong folder
514 lines
13 KiB
Bash
Executable file
514 lines
13 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
# Dependencies
|
|
# * bash (includes bash extentions that are not in posix shell eg [[ ]]
|
|
# * sqlite3
|
|
# * sed
|
|
# * awk
|
|
# * tr
|
|
# * git
|
|
# * yq (https://github.com/kislyuk/yq)
|
|
# * fzf (https://github.com/junegunn/fzf)
|
|
# * rg (https://github.com/BurntSushi/ripgrep)
|
|
# * bat (https://github.com/sharkdp/bat)
|
|
|
|
|
|
|
|
# Colour definitions
|
|
RED='\033[1;31m'
|
|
GREEN='\033[1;32m'
|
|
YELLOW='\033[1;33m'
|
|
WHITE='\033[1;37m'
|
|
NC='\033[0m'
|
|
|
|
# Provide a variable with the location of this script.
|
|
scriptPath="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
|
|
# Source files
|
|
# ##################################################
|
|
|
|
source "inc/init"
|
|
source "inc/tag-management"
|
|
source "inc/file-management"
|
|
source "inc/yaml"
|
|
source "inc/fzf"
|
|
source "inc/to-html"
|
|
|
|
# Utility functions
|
|
# ##################################################
|
|
|
|
die(){
|
|
necho -e "${RED}$*${NC}" >&2
|
|
exit 1
|
|
}
|
|
|
|
warn(){
|
|
necho -e "${YELLOW}$*${NC}" > /dev/tty
|
|
}
|
|
|
|
# Normal echo
|
|
# Echo if quiet is 0
|
|
necho(){
|
|
if [ "$quiet" -eq 0 ]; then
|
|
echo "$@"
|
|
fi
|
|
}
|
|
|
|
# verbose echo
|
|
# Echo if verbose is 1
|
|
vecho(){
|
|
if [ "$verbose" -eq 1 ]; then
|
|
echo "$@" > /dev/tty
|
|
fi
|
|
}
|
|
|
|
# Exits after any cleanup that may be needed
|
|
safeExit(){
|
|
# TODO: Add cleanup
|
|
exit 0
|
|
}
|
|
|
|
|
|
isInt() {
|
|
[ "$1" -eq "$1" ] 2> /dev/null
|
|
return "$?"
|
|
}
|
|
|
|
# Escapes ' and \ characters
|
|
safeSQL(){
|
|
echo "$1" |
|
|
sed 's/\\/\\\\/g' |
|
|
sed "s/'/\\\'/g"
|
|
}
|
|
|
|
|
|
# Set Flags
|
|
# -----------------------------------
|
|
# Flags which can be overridden by user input.
|
|
# Default values are below
|
|
# -----------------------------------
|
|
quiet=0
|
|
verbose=0
|
|
debug=0
|
|
dogit=1
|
|
args=()
|
|
dataDir="${XDG_DATA_HOME:=$HOME/.local/share}/kb/"
|
|
sqliteFile=""
|
|
editor="${EDITOR:=vim}"
|
|
if type -p bat > /dev/null 2>&1; then
|
|
pager="bat"
|
|
else
|
|
pager="${PAGER:=cat}"
|
|
fi
|
|
|
|
# Check for Dependencies
|
|
# -----------------------------------
|
|
# Arrays containing package dependencies needed to execute this script.
|
|
# The script will fail if dependencies are not installed.
|
|
# -----------------------------------
|
|
checkDependencies(){
|
|
local dependencies=(
|
|
"sqlite3"
|
|
"rg"
|
|
"fzf"
|
|
"sed"
|
|
"awk"
|
|
"tr"
|
|
"git"
|
|
"yq"
|
|
"bat"
|
|
"git-lfs"
|
|
)
|
|
local ret=0
|
|
for program in "${dependencies[@]}"; do
|
|
if ! type "$program" &>/dev/null; then
|
|
necho -e "${RED}$program is not installed${NC}"
|
|
ret=1
|
|
case "$program" in
|
|
"rg")
|
|
necho -e "\t${YELLOW}This provides effecient searching${NC}"
|
|
;;
|
|
"fzf")
|
|
necho -e "\t${YELLOW}This provides fuzzy searching${NC}"
|
|
;;
|
|
|
|
esac
|
|
fi
|
|
done
|
|
return $ret
|
|
}
|
|
|
|
# This will create a fresh database based on the files in the folder
|
|
makedb(){
|
|
[ -f "$sqliteFile" ] && die "sqlite file already exists\ndelete it first if you want to"
|
|
echo "Still need to implement this"
|
|
}
|
|
|
|
|
|
# Makes file names safe
|
|
escapeFilename(){
|
|
vecho "escapeFilename $*"
|
|
if [[ "$1" = assets/* ]]; then
|
|
echo -n "assets/"
|
|
echo "$(escapeFilename "${1#*/}")"
|
|
#echo "assets/$(escapeFilename "${1#*/}")"
|
|
else
|
|
echo "$1" |
|
|
tr ' ' '_' | # replace spaces with underscores
|
|
tr '/' '_' # replace slashes with underscores
|
|
fi
|
|
}
|
|
|
|
findFileId(){
|
|
local filename
|
|
filename="$(findFile "$1")"
|
|
[ ! -e "$filename" ] && exit 1
|
|
echo "SELECT id FROM items WHERE filename = '$(safeSQL "$filename")'" |
|
|
sqlite3 "${sqliteFile}"
|
|
|
|
}
|
|
|
|
findFile(){
|
|
vecho "findFile $*"
|
|
cd "$dataDir" || return
|
|
local filename
|
|
local options
|
|
local count
|
|
|
|
if [ "$#" -eq 1 ] && isInt "$1"; then
|
|
# if it's an integer, try to find an entry with that id
|
|
filename="$(echo "SELECT filename
|
|
FROM items WHERE id = '$(safeSQL "$1")'" |
|
|
sqlite3 "${sqliteFile}")"
|
|
fi
|
|
|
|
[ -z "$filename" ] && filename="$(escapeFilename "$*")"
|
|
if [ -e "$filename" ]; then
|
|
# If the file exists, return it
|
|
echo "$filename"
|
|
exit 0
|
|
else
|
|
# If it exists with .md at the end, assume that was meant
|
|
# otherwise die
|
|
if [ -e "$filename.md" ]; then
|
|
echo "$filename.md"
|
|
exit 0
|
|
else
|
|
# If we get here, try to find it in the db
|
|
options="$( echo "SELECT filename from items
|
|
WHERE title LIKE '%$(safeSQL $filename)%'
|
|
UNION SELECT filename from items
|
|
WHERE filename LIKE '%$(safeSQL $filename)%'" |
|
|
sqlite3 "${sqliteFile}" )"
|
|
count="$(echo "$options" | wc -l)"
|
|
if [ "$count" -eq 0 ]; then
|
|
die "No such file or ID '$filename'"
|
|
elif [ "$count" -eq 1 ]; then
|
|
echo -n "$options"
|
|
else
|
|
die "Not a unique substring\n\n${NC}Could have been any of:
|
|
$options"
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
fileInDB(){
|
|
local ids
|
|
ids="$(echo "SELECT id FROM items WHERE filename = '$(safeSQL "$1")'" |
|
|
sqlite3 "${sqliteFile}")"
|
|
[ -n "$ids" ] && return 0
|
|
return 1
|
|
}
|
|
|
|
externalgit(){
|
|
cd "$dataDir" || return
|
|
git "$@"
|
|
}
|
|
|
|
# This function will add and commit a file after it has been edited
|
|
# If 2 arguments are given, it assumes a rename has taken place.
|
|
# The first should be the old file (like mv)
|
|
gitChange(){
|
|
cd "$dataDir" || return
|
|
local filename="$1"
|
|
local newFilename="$2"
|
|
if [ "$dogit" -gt 0 ]; then
|
|
if [ -f "$filename" ]; then
|
|
if ! git diff --exit-code "$filename" > /dev/null 2>&1; then
|
|
# Changes
|
|
git add "$filename"
|
|
[ -e "$filename.yaml" ] && git add "$filename.yaml"
|
|
git commit -m "KB auto-commit: Updated: $filename"
|
|
elif [ -e "$filename.yaml" ] && ! git diff --exit-code "$filename.yaml" > /dev/null 2>&1; then
|
|
git add "$filename.yaml"
|
|
git commit -m "KB auto-commit: Updated: $filename.yaml"
|
|
elif ! git ls-files --error-unmatch "$filename" > /dev/null 2>&1; then
|
|
# New file
|
|
git add "$filename"
|
|
[ -e "$filename.yaml" ] && git add "$filename.yaml"
|
|
git commit -m "KB auto-commit: New: $filename"
|
|
fi
|
|
else
|
|
# If the file name doesn't exist, we have probably moved it
|
|
if [ -n "$newFilename" ] && [ -f "$newFilename" ]; then
|
|
git add "$filename" "$newFilename"
|
|
git commit -m "KB auto-commit: move: $filename -> $newFilename"
|
|
else
|
|
# if we get here, the file has been deleted
|
|
git add "$filename"
|
|
git add "$filename.yaml" 2> /dev/null || true
|
|
git commit -m "KB auto-commit: delete: $filename"
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
listEntries(){
|
|
vecho "listEntries $*"
|
|
cd "$dataDir" || return
|
|
local header="--header"
|
|
local typeorlist=""
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--noheader) header="" ;;
|
|
--normal) typeorlist+=" OR items.type = 'normal'" ;;
|
|
--asset|--assets) typeorlist+=" OR items.type = 'asset'" ;;
|
|
--links) typeorlist+=" OR items.type = 'links'" ;;
|
|
*) die "invalid list option: '$1'." ;;
|
|
esac
|
|
shift
|
|
done
|
|
typeorlist="$(echo "$typeorlist" | sed 's/^ OR //')"
|
|
[ -z "$typeorlist" ] && typeorlist="items.type = 'normal'"
|
|
|
|
echo "SELECT items.id,items.filename,items.title,items.type,
|
|
GROUP_CONCAT(tags.name,',') tags
|
|
FROM items LEFT JOIN links ON items.id = links.itemID
|
|
LEFT JOIN tags ON links.tagID = tags.id
|
|
WHERE $typeorlist
|
|
GROUP BY items.id;" |
|
|
sqlite3 --column $header "${sqliteFile}"
|
|
}
|
|
|
|
|
|
indexFolder(){
|
|
vecho "indexFolder $*"
|
|
local olddogit="$dogit"
|
|
dogit=0
|
|
initDB
|
|
find "$dataDir" -name "*.md" | while read file; do
|
|
file="${file##*/}"
|
|
updateFileChange "$file"
|
|
done
|
|
dogit="$olddogit"
|
|
|
|
}
|
|
|
|
convert(){
|
|
local destination
|
|
local method
|
|
|
|
while [[ $1 = -?* ]]; do
|
|
case "$1" in
|
|
--to-html) method="to-html"; destination="html" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
[ -n "$1" ] && destination="$1"
|
|
|
|
[ -z "$method" ] && die "You must specify a conversion method"
|
|
[ -z "$destination" ] && die "You must specify a destination"
|
|
[ -d "$destination" ] || die "$destination must be a directory"
|
|
|
|
"$method" "$destination"
|
|
}
|
|
|
|
|
|
mainScript() {
|
|
############## Begin Script Here ###################
|
|
####################################################
|
|
if [ "${args[0]}" != "init" ]; then
|
|
olddir="$PWD"
|
|
cd "$dataDir" || return
|
|
# Check to see if datadir is a git repo
|
|
if ! git rev-parse 2> /dev/null; then
|
|
# If not, ensure we don't run git commands
|
|
dogit=0
|
|
fi
|
|
cd "$olddir" || return
|
|
fi
|
|
|
|
case "${args[0]}" in
|
|
add) addFile "${args[@]:1}"; safeExit ;;
|
|
convert) convert "${args[@]:1}"; safeExit ;;
|
|
deepsearch) deepSearch "${args[@]:1}"; safeExit ;;
|
|
del|delete) deleteFile "${args[@]:1}"; safeExit ;;
|
|
edit) editFile "${args[@]:1}"; safeExit ;;
|
|
fuzzy) fuzzySelect "${args[@]:1}"; safeExit ;;
|
|
git) externalgit "${args[@]:1}"; safeExit ;;
|
|
index) indexFolder "${args[@]:1}"; safeExit ;;
|
|
init) initKnowledgeBase; safeExit ;;
|
|
list) listEntries "${args[@]:1}"; safeExit ;;
|
|
list-tags) listTags "${args[@]:1}"; safeExit ;;
|
|
makedb) makedb; safeExit ;;
|
|
new) newFile "${args[@]:1}"; safeExit ;;
|
|
purge-tags) purgeTags "${args[@]:1}"; safeExit ;;
|
|
update) updateFileChange "${args[@]:1}"; safeExit ;;
|
|
view) viewFile "${args[@]:1}"; safeExit ;;
|
|
|
|
*) usage >&2; safeExit ;;
|
|
esac
|
|
|
|
|
|
####################################################
|
|
############### End Script Here ####################
|
|
}
|
|
|
|
############## Begin Options and Usage ###################
|
|
|
|
|
|
# Print usage
|
|
usage() {
|
|
echo -n "kb [OPTIONS]... COMMAND
|
|
|
|
Knowledge Base Management
|
|
|
|
This tool helps manage my personal knowledge base
|
|
|
|
Options:
|
|
-q, --quiet Quiet (no output)
|
|
-v, --verbose Output more information. (Items echoed to 'verbose')
|
|
-d, --debug Runs script in BASH debug mode (set -x)
|
|
-h, --help Display this help and exit
|
|
--data <directory> The knowledgebase data dir
|
|
--sqlite <file> The sqlite file (default to <data>/knowledgebase.sqlite3
|
|
--editor <editor> The editor to use (default $EDITOR or vim)
|
|
--pager <editor> The pager to use (default bat or $PAGER or cat)
|
|
--nogit Don't run git commands
|
|
--version Output version information and exit
|
|
|
|
Commands:
|
|
init Initialise a new knowledgebase
|
|
new [options] [title] Create new entry
|
|
--filetype <type> Type of file ( default md)
|
|
edit [title] Edit a file
|
|
list List all files
|
|
--noheader Don't include the header
|
|
--normal List items of type \"normal\"
|
|
list-tags Lists tags with the number of times its used
|
|
--noheader Don't include the header
|
|
purge-tags Deletes any unused tags
|
|
update <file> [<file>] Updates the database and git repo of a changed file
|
|
If 2 files are given, it assumes a move
|
|
view View a file
|
|
add [options] <file> Adds a file
|
|
--asset Adds the file as an asset
|
|
--yaml-header Adds a yaml header (default for markdown files)
|
|
--yaml-file Adds a yaml file (default for non-markdown files)
|
|
fuzzy [default] Fuzzy select a file
|
|
git [options] Run arbitary git commands on the kb repository
|
|
del [title] Delete a file
|
|
index Indexes the folder (usful after a clone etc)
|
|
convert [options] [dest] Converts the notes into a different format and puts it in dest
|
|
--to-html Converts to html
|
|
"
|
|
}
|
|
|
|
# Iterate over options breaking -ab into -a -b when needed and --foo=bar into
|
|
# --foo bar
|
|
optstring=h
|
|
unset options
|
|
while (($#)); do
|
|
case $1 in
|
|
# If option is of type -ab
|
|
-[!-]?*)
|
|
# Loop over each character starting with the second
|
|
for ((i=1; i < ${#1}; i++)); do
|
|
c=${1:i:1}
|
|
|
|
# Add current char to options
|
|
options+=("-$c")
|
|
|
|
# If option takes a required argument, and it's not the last char make
|
|
# the rest of the string its argument
|
|
if [[ $optstring = *"$c:"* && ${1:i+1} ]]; then
|
|
options+=("${1:i+1}")
|
|
break
|
|
fi
|
|
done
|
|
;;
|
|
|
|
# If option is of type --foo=bar
|
|
--?*=*) options+=("${1%%=*}" "${1#*=}") ;;
|
|
# add --endopts for --
|
|
--) options+=(--endopts) ;;
|
|
# Otherwise, nothing special
|
|
*) options+=("$1") ;;
|
|
esac
|
|
shift
|
|
done
|
|
set -- "${options[@]}"
|
|
unset options
|
|
|
|
# Print help if no arguments were passed.
|
|
# Uncomment to force arguments when invoking the script
|
|
#[[ $# -eq 0 ]] && set -- "--help"
|
|
|
|
# Read the options and set stuff
|
|
while [[ $1 = -?* ]]; do
|
|
case $1 in
|
|
-h|--help) usage >&2; safeExit ;;
|
|
-v|--verbose) verbose=1 ;;
|
|
-q|--quiet) quiet=1 ;;
|
|
-d|--debug) debug=1;;
|
|
# Ensure that the dataDir has a trailing slash
|
|
--data) dataDir="${2%%/}/"; shift ;;
|
|
--sqlite) sqliteFile="$2"; shift ;;
|
|
--editor) editor="$2"; shift ;;
|
|
--pager) pager="$2"; shift ;;
|
|
--nogit) dogit=0 ;;
|
|
--) shift; break ;;
|
|
*) die "invalid option: '$1'." ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# If the sqlite file hasn't been specified on the command line, make it a file
|
|
# called knowledgebase.sqlite3 in the dataDir
|
|
[ -z "$sqliteFile" ] && sqliteFile="${dataDir}knowledgebase.sqlite3"
|
|
|
|
# Store the remaining part as arguments.
|
|
args+=("$@")
|
|
|
|
# if no arguments or options are passed, assume fuzzy as default
|
|
[ "${#args[@]}" -eq 0 ] && args+=("fuzzy")
|
|
|
|
|
|
############## End Options and Usage ###################
|
|
|
|
|
|
|
|
|
|
# Exit on error. Append '||true' when you run the script if you expect an error.
|
|
set -o errexit
|
|
|
|
# Run in debug mode, if set
|
|
if [ "${debug}" == "1" ]; then
|
|
set -x
|
|
fi
|
|
|
|
# Bash will remember & return the highest exitcode in a chain of pipes.
|
|
# This way you can catch the error in case mysqldump fails in `mysqldump |gzip`, for example.
|
|
set -o pipefail
|
|
|
|
# Invoke the checkDependencies function to test for Bash packages
|
|
checkDependencies
|
|
|
|
|
|
# Run your script
|
|
mainScript
|
|
|
|
safeExit # Exit cleanly
|
|
|