429 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			429 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+=("$@")
 | |
| 
 | |
| ############## 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
 | |
| 
 |