CLOSED: [2016-04-16 Sat 15:15]
As a person who has a lot of books, I like to be able to keep track of them. I've tried other pre-built solutions, but they've been flaky and don't do well for the types of books I have. I've also tried to use basic spreadsheets, but in the end, they've not been able to keep up with my desire to search through or produce reports. So, when I stumbled on GNU recutils, I decided that building a script around it would be useful. This is the script and system.
COMMAND=$1
shift
case "${COMMAND}" in
help)
display_help
exit
;;
query)
run_query "$@"
exit
;;
add)
add_single "$@"
exit
;;
git)
do_git "$@"
exit
;;
bulk-add)
bulk_add "$@"
exit
;;
report)
do_report "$@"
exit
;;
edit)
do_edit "$@"
exit
;;
edit-matching)
do_edit_exp "$@"
exit
;;
loan)
do_loan "$@"
exit
;;
return-book)
do_return "$@"
exit
;;
withdraw)
do_withdraw "$@"
exit
;;
drop-withdrawn)
remove_withdrawn "$@"
exit
;;
init)
initialize
exit
;;
,*)
display_help
exit
esac
CLOSED: [2016-10-26 Wed 18:02]
The following defines the format of book records, which are used to describe each individual book in the library. A record contains the following:
an integer, automatically generated by the system.
A string, at most, one line long.
A string, in the format of Last, First && Second Last, First.
A string, being the LOC Classification of the given book.
A string, made up of the numbers 0-9, with an optional "X" at the end.
A string, generally unformatted.
An integer, the year the book was published, or the copyright, whichever is later.
A string denoting where the book is located.
An optional timestamp denoting when a book was withdrawn from the library.
A timestamp representing when the book was inserted into the library, this is automatically generated.
A string, who the book is currently loaned to. Non-existent if not on loan.
A timestamp, when the book was loaned. Non-existent if not on loan.
# -*- mode: rec -*- %rec: Book %doc: Foo %key: ID %unique: Title %type: ID int %type: Title line %type: Author line %type: LCCN line %type: ISBN regexp /[0-9]*X?/ %type: Publisher line %type: Copyright int %type: Location line %type: Withdrawn date %type: Inserted date %type: Course line %type: LoanTo line %type: LoanOn date %typedef: CardPrint enum PRINTED REPRINT UNPRINTED %type: Card CardPrint %mandatory: Title Author LCCN Inserted %allowed: ISBN Publisher Copyright Location Withdrawn LoanTo LoanOn Course Card %auto: ID Inserted
CLOSED: [2016-10-23 Sun 15:01]
The initialization of the database is accomplished by creating the containing directory, and within it, creating the database file, which defines the structure of the database. If git is to be initialized for the database, this must be done manually.
function initialize {
OLD=`pwd`
mkdir -p "${LIBRARY_DIRECTORY}"
cd "${LIBRARY_DIRECTORY}"
[[ ! -e `basename "${LIBRARY_FILE}"` ]] && \
cat <<EOF > `basename "${LIBRARY_FILE}"`
<<file-format>>
EOF
}
CLOSED: [2016-10-23 Sun 15:06]
This handles git as needed, by first checking to see if the first argument is init, and if so, initializing the library directory as a repository. Otherwise, if the GIT variable is detect, it runs the specified git command within the library directory if possible.
function do_git {
if [[ $# -lt 1 ]] ; then
echo "library git args*"
exit 1
fi
FIRST=$1
if [[ $FIRST == "init" ]] ; then
OLD=`pwd`
cd "${LIBRARY_DIRECTORY}"
git "$@"
cd ${OLD}
else
if [[ $LIBRARY_GIT == "detect" ]] ; then
OLD=`pwd`
cd "${LIBRARY_DIRECTORY}"
git "$@"
cd ${OLD}
fi
fi
}
function run_query {
recsel -t Book "$@" "${LIBRARY_FILE}"
}
function add_single {
if [[ $# -lt 7 ]] ; then
read -e -p "Title: " TITLE
read -e -p "Author: " AUTHOR
read -e -p "LCCN: " LCCN
read -e -p "Copyright Year: " COPYRIGHT
read -e -p "Publisher: " PUBLISHER
read -e -p "ISBN: " ISBN
read -e -p "Location: " LOCATION
else
TITLE=$1
shift
AUTHOR=$1
shift
LCCN=$1
shift
COPYRIGHT=$1
shift
PUBLISHER=$1
shift
ISBN=$1
shift
LOCATION=$1
shift
fi
TMPDIR=. recins -t Book \
-f Title -v "${TITLE}" \
-f Author -v "${AUTHOR}" \
-f LCCN -v "${LCCN}" \
-f Copyright -v "${COPYRIGHT}" \
-f Publisher -v "${PUBLISHER}" \
-f ISBN -v "${ISBN}" \
-f Location -v "${LOCATION}" \
-f Card -v UNPRINTED \
"${LIBRARY_FILE}"
do_git add `basename "${LIBRARY_FILE}"`
do_git commit -m "Added record for \"${TITLE}\""
library query -i --uniq -e "Title ~ \"${TITLE}\"" | less
}
function bulk_add {
if [[ $@ -lt 1 ]] ; then
echo "library bulk-add number"
exit 1
fi
GITOLD="${LIBRARY_GIT}"
LIBRARY_GIT=FALSE
for i in 1 .. $1 ; do
echo "Adding book number ${i}"
add_single
done
LIBRARY_GIT="${GITOLD}"
do_git add `basename "${LIBRARY_FILE}"`
do_git commit -m "Added ${1} records"
}
function do_report {
if [[ $# -lt 1 ]] ; then
echo "library report name args*"
exit 1
fi
NAME=$1
shift
case "${NAME}" in
list)
for report in ${LIBRARY_DIRECTORY}/reports/*.report ;
do
echo " - $(basename -- "${report}" .report)"
done
;;
new)
if [[ $# -lt 1 ]] ; then
echo "library report new name"
exit 1
fi
REPORT=$1
shift
echo "# -*- mode: shell-script -*-" > "${LIBRARY_DIRECTORY}/reports/${REPORT}.report"
emacsclient --alternate-editor="" -n "${LIBRARY_DIRECTORY}/reports/${REPORT}.report"
;;
,*)
if [[ -e "${LIBRARY_DIRECTORY}/reports/${NAME}.report" ]] ; then
sh "${LIBRARY_DIRECTORY}/reports/${NAME}.report" "$@"
fi
esac
}
function do_edit {
if [[ $# -lt 2 ]] ; then
echo "library edit id field [ value ]"
exit 1
fi
ID=$1
shift
FIELD=$1
shift
VALUE=$1
shift
TMPDIR=. recset -e "ID = ${ID}" \
-f "${FIELD}" -S "${VALUE}" \
"${LIBRARY_FILE}"
do_git add `basename "${LIBRARY_FILE}"`
do_git commit -m "Edited record id ${ID}"
}
function do_edit_exp {
if [[ $# -lt 3 ]] ; then
echo "library edit-matching match-exp field value [ commit-message ]"
exit 1
fi
MATCHEXPRESSION=$1
shift
FIELDNAME=$1
shift
VALUE=$1
shift
TMPDIR=. recset -e "${MATCHEXPRESSION}" \
-f "${FIELDNAME}" -S "${VALUE}" \
"${LIBRARY_FILE}"
do_git add $(basename "${LIBRARY_FILE}")
if [[ $1 != "" ]] ; then
do_git commit -m "${1}"
else
do_git commit -m "Bulk edited records"
fi
}
function do_withdraw {
if [ $# -lt 1 ] ; then
echo "library withdraw id"
exit 1
fi
ID=$1
TMPDIR=. recset -e "ID = ${ID}" \
-f "Withdrawn" -S "`date +"%a, %d %b %Y %H:%M:%S %z"`" \
-f "Location" -S "WITHDRAWN" \
"${LIBRARY_FILE}"
do_git add $(basename ${LIBRARY_FILE})
do_git commit -m "Withdrew book ${ID}"
}
function remove_withdrawn {
TMPDIR=. recdel -t Book \
-e "Location ~ \"WITHDRAWN\"" \
"${LIBRARY_FILE}"
do_git add $(basename ${LIBRARY_FILE})
do_git commit -m "Remove Withdrawn Books"
}
function do_loan {
if [[ $# -lt 2 ]] ; then
echo "library loan id name"
exit 1
fi
ID=$1
shift
NAME=$1
shift
TMPDIR=. recset -e "ID = ${ID}" \
-f "LoanTo" -S "${NAME}" \
"${LIBRARY_FILE}"
TMPDIR=. recset -e "ID = ${ID}" \
-f "LoanOn" -S "`date +"%a, %d %b %Y %H:%M:%S %z"`" \
"${LIBRARY_FILE}"
do_git add `basename "${LIBRARY_FILE}"`
do_git commit -m "Loaned Book ${ID} to ${NAME}"
}
function do_return {
if [[ $# -lt 1 ]] ; then
echo "library return-book id"
exit 1
fi
ID=$1
shift
TMPDIR=. recset -e "ID = ${ID}" \
-f "LoanTo" -d \
"${LIBRARY_FILE}"
TMPDIR=. recset -e "ID = ${ID}" \
-f "LoanOn" -d \
"${LIBRARY_FILE}"
do_git add `basename "${LIBRARY_FILE}"`
do_git commit -m "Returned Book ${ID}"
}
export LIBRARY_FILE=${LIBRARY_FILE:-~/.library/library.rec}
export LIBRARY_DIRECTORY=${LIBRARY_DIRECTOR:-~/.library}
export LIBRARY_GIT=${LIBRARY_GIT:-detect}
export LIBRARY_DEFAULT_FIELDS=${LIBRARY_DEFAULT_FIELDS:-ID,Title,Author,LCCN,Location,Course}
CLOSED: [2016-11-06 Sun 11:25]
To complete this program, I include a help message, a small part of which is displayed if there are no arguments passed.
if [[ $# -eq 0 ]] ; then
echo "library [ help | query | add | git | bulk-add | report | edit | edit-matching | loan | return-book | withdraw | drop-withdrawn | init ]"
exit
fi
function display_help {
cat <<EOF
library [ help | query | add | git | bulk-add | report | edit | edit-matching | loan | return-book | init ]
help: Display this help message.
query: Query Library Database.
add: Add a singular book record.
git: Run a git command.
bulk-add: Add a specified number of records.
report: Run a report.
edit: Edit the value of a specified field in a specified record.
edit-matching: Edit records matching a give expression
loan: Loan a book out.
return-book:Process a book return
withdraw: Withdraw a specified book
drop-withdrawn: Drop withdrawn books
init: Initialize the database.
EOF
}
CLOSED: [2016-04-16 Sat 15:20]
Finally, this is what puts the application together. Placing each function in its place, making sure that everything is included is done here, fairly simply, by referencing the code block and being expanded here.
<<configuration>> <<help-message>> <<handle-reports>> <<handle-git>> <<handle-query>> <<add-book>> <<add-in-bulk>> <<edit-field>> <<withdraw-books>> <<initialize-database>> <<loan>> <<process-commands>>