#+Title: Library Management for Small Users #+Subtitle: Simple software for average users #+AUTHOR: Samuel W. Flint #+EMAIL: swflint@flintfam.org #+DATE: \today #+INFOJS_OPT: view:info toc:nil path:http://flintfam.org/org-info.js #+OPTIONS: toc:nil H:5 ':t *:t todo:nil stat:nil d:nil #+PROPERTY: header-args :noweb no-export :comments noweb #+LATEX_HEADER: \usepackage[margin=0.75in]{geometry} #+LATEX_HEADER: \lstset{texcl=true,breaklines=true,columns=fullflexible,basicstyle=\ttfamily,frame=lines,literate={<=}{$\leq$}1 {>=}{$\geq$}1} #+LATEX_CLASS_OPTIONS: [10pt,twoside,paper=letter] * Export :noexport: :PROPERTIES: :CREATED: <2016-10-06 Thu 14:21> :END: #+Caption: Export Document #+Name: export-document #+BEGIN_SRC emacs-lisp :exports none :results none (save-buffer) (let ((org-confirm-babel-evaluate (lambda (lang body) (declare (ignorable lang body)) nil))) (org-latex-export-to-pdf)) #+END_SRC * Tangle :noexport: :PROPERTIES: :CREATED: <2016-10-06 Thu 14:21> :END: #+Caption: Tangle Document #+Name: tangle-document #+BEGIN_SRC emacs-lisp :exports none :results none (save-buffer) (let ((python-indent-offset 4)) (org-babel-tangle)) #+END_SRC * DONE Introduction CLOSED: [2016-04-16 Sat 15:15] :PROPERTIES: :CREATED: <2016-04-13 Wed 20:15> :UNNUMBERED: t :END: 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. * TOC :ignore: :PROPERTIES: :CREATED: <2016-04-13 Wed 20:15> :END: #+TOC: headlines 3 #+TOC: listings * WORKING Process Commands :PROPERTIES: :CREATED: <2016-04-13 Wed 22:07> :ID: 9d10ce06-45f0-4957-85c3-b8fcfff42da8 :END: #+Caption: Process Commands #+Name: process-commands #+BEGIN_SRC sh 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 #+END_SRC * DONE File Format CLOSED: [2016-10-26 Wed 18:02] :PROPERTIES: :CREATED: <2016-04-13 Wed 20:17> :ID: 1745de61-6511-4257-bed8-112df2362fe7 :END: The following defines the format of book records, which are used to describe each individual book in the library. A record contains the following: - ID :: an integer, automatically generated by the system. - Title :: A string, at most, one line long. - Author :: A string, in the format of ~Last, First && Second Last, First~. - LCCN :: A string, being the LOC Classification of the given book. - ISBN :: A string, made up of the numbers 0-9, with an optional "X" at the end. - Publisher :: A string, generally unformatted. - Copyright :: An integer, the year the book was published, or the copyright, whichever is later. - Location :: A string denoting where the book is located. - Withdrawn :: An optional timestamp denoting when a book was withdrawn from the library. - Inserted :: A timestamp representing when the book was inserted into the library, this is automatically generated. - LoanTo :: A string, who the book is currently loaned to. Non-existent if not on loan. - LoanOn :: A timestamp, when the book was loaned. Non-existent if not on loan. #+Caption: File Format #+Name: file-format #+BEGIN_SRC text # -*- 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 #+END_SRC * DONE Initialize CLOSED: [2016-10-23 Sun 15:01] :PROPERTIES: :CREATED: <2016-04-13 Wed 22:13> :ID: 8308ab62-05a0-4240-a073-d2e4baab9ab2 :END: 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. #+Caption: Initialize a Database #+Name: initialize-database #+BEGIN_SRC sh function initialize { OLD=`pwd` mkdir -p "${LIBRARY_DIRECTORY}" cd "${LIBRARY_DIRECTORY}" [[ ! -e `basename "${LIBRARY_FILE}"` ]] && \ cat < `basename "${LIBRARY_FILE}"` <> EOF } #+END_SRC * DONE Handle Git CLOSED: [2016-10-23 Sun 15:06] :PROPERTIES: :CREATED: <2016-04-13 Wed 20:17> :ID: 68ee8e3d-3280-4280-8dd4-e824b5c81929 :END: 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. #+Caption: Handle Git #+Name: handle-git #+BEGIN_SRC sh 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 } #+END_SRC * WORKING Run Queries :PROPERTIES: :CREATED: <2016-04-13 Wed 20:17> :ID: 9cc04c91-08ea-49db-a0ac-f3789e22ff33 :END: #+Caption: Handle Query #+Name: handle-query #+BEGIN_SRC sh function run_query { recsel -t Book "$@" "${LIBRARY_FILE}" } #+END_SRC * WORKING Add Book :PROPERTIES: :CREATED: <2016-04-13 Wed 20:17> :ID: 7b842d87-7f13-483e-96ee-5a318877eeb7 :END: #+Caption: Add Book #+Name: add-book #+BEGIN_SRC sh 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 } #+END_SRC * WORKING Add Books in Bulk :PROPERTIES: :CREATED: <2016-04-13 Wed 20:17> :ID: 2316a78c-88d7-4629-b310-b6b0c8b848b6 :END: #+Caption: Add Books in Bulk #+Name: add-in-bulk #+BEGIN_SRC sh 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" } #+END_SRC * WORKING Handle Reporting :PROPERTIES: :CREATED: <2016-04-13 Wed 20:18> :ID: 1e6d79cb-aa8d-4a44-9268-5c5d502c5c38 :END: #+Caption: Reporting #+Name: handle-reports #+BEGIN_SRC sh 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 } #+END_SRC * WORKING Edit Field :PROPERTIES: :CREATED: <2016-04-15 Fri 11:50> :ID: 56281ec3-8323-4f9c-892f-7edaff86393f :END: #+Caption: Edit Fields #+Name: edit-field #+BEGIN_SRC sh 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 } #+END_SRC * WORKING Withdrawing Books :PROPERTIES: :ID: 190e248e-4102-431e-ba8b-288e71c10660 :END: #+Caption: Withdrawing Books #+Name: withdraw-books #+BEGIN_SRC sh 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" } #+END_SRC * WORKING Loan :PROPERTIES: :CREATED: <2016-10-24 Mon 19:39> :ID: 42dddddc-ab8b-435c-920a-df4463b3c246 :END: #+Caption: Handle Loaning #+Name: loan #+BEGIN_SRC sh 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}" } #+END_SRC * TODO Configuration :PROPERTIES: :ID: 86ea3306-d341-4580-9fd6-be04e3f723b2 :END: #+Caption: Configuration #+Name: configuration #+BEGIN_SRC sh 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} #+END_SRC * DONE Help Message CLOSED: [2016-11-06 Sun 11:25] :PROPERTIES: :CREATED: <2016-04-13 Wed 22:04> :ID: 3d90ee25-d8af-4590-b6bc-80e0430299f2 :END: To complete this program, I include a help message, a small part of which is displayed if there are no arguments passed. #+Caption: Help Message #+Name: help-message #+BEGIN_SRC sh 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 < :ID: 57c4d5f0-9fc7-41bd-a2f6-1ab6b8d12ae6 :END: 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. #+Caption: Packaging #+Name: packaging #+BEGIN_SRC sh :tangle "~/bin/library" :shebang "#!/usr/bin/env bash" <> <> <> <> <> <> <> <> <> <> <> <> #+END_SRC