If no arguments are passed, it will open the fuzzy search From here you can search, create and delete
434 lines
10 KiB
Bash
Executable file
434 lines
10 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"
|
|
|
|
# Utility functions
|
|
# ##################################################
|
|
|
|
die(){
|
|
necho -e "${RED}$*${NC}" >&2
|
|
exit 1
|
|
}
|
|
|
|
# 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 "$@"
|
|
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"
|
|
)
|
|
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 $*"
|
|
echo "$1" |
|
|
tr ' ' '_' | # replace spaces with underscores
|
|
tr -d '/' # Delete slashes
|
|
}
|
|
|
|
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
|
|
|
|
if [ "$#" -eq 1 ] && isInt "$1"; then
|
|
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
|
|
die "No such file or ID $filename"
|
|
exit 1
|
|
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"
|
|
git commit -m "KB auto-commit: Updated: $filename"
|
|
elif ! git ls-files --error-unmatch "$filename" > /dev/null 2>&1; then
|
|
# New file
|
|
git add "$filename"
|
|
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 commit -m "KB auto-commit: delete: $filename"
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
listEntries(){
|
|
vecho "listEntries $*"
|
|
cd "$dataDir" || return
|
|
local header="--header"
|
|
[ "$1" = "--noheader" ] && header=""
|
|
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
|
|
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"
|
|
|
|
}
|
|
|
|
|
|
mainScript() {
|
|
############## Begin Script Here ###################
|
|
####################################################
|
|
if [ "${args[0]}" != "init" ]; then
|
|
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
|
|
fi
|
|
|
|
case "${args[0]}" in
|
|
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 ;;
|
|
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
|
|
list-tags Lists tags with the number of times its used
|
|
--noheader Don't include the header
|
|
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
|
|
fuzzy [command] Fuzzy select a file
|
|
command is what to do with the selected file
|
|
edit or view
|
|
git [options] Run arbitary git commands on the kb repository
|
|
del [title] Delete a file
|
|
index Indexes the folder (usful after a clone etc)
|
|
"
|
|
}
|
|
|
|
# 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;;
|
|
--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
|
|
|