library.org 14 KB

Library Management for Small Users

Introduction

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.

TOC   ignore

WORKING Process Commands

  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
          ;;
      init)
          initialize
          exit
          ;;
      ,*)
          display_help
          exit
  esac

File Format

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:

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.

  # -*- 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: LoanTo line
  %type: LoanOn date
  %mandatory: Title Author LCCN Inserted
  %allowed: ISBN Publisher Copyright Location Withdrawn LoanTo LoanOn
  %auto: ID Inserted

Initialize

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 ${LIBRARYDIRECTORY}
      cd ${LIBRARYDIRECTORY}
      [[ ! -e `basename ${LIBRARYFILE}` ]] && \
          cat <<EOF > `basename ${LIBRARYFILE}`
  <<file-format>>
  EOF
  }

Handle Git

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 ${LIBRARYDIRECTORY}
          git $@
          cd ${OLD}
      else
          if [[ $GIT == "detect" ]] ; then
              OLD=`pwd`
              cd ${LIBRARYDIRECTORY}
              git "$@"
              cd ${OLD}
          fi
      fi
  }

WORKING Run Queries

  function run_query {
      recsel -t Book $@ ${LIBRARYFILE}
  }

WORKING Add Book

  function add_single {
      if [[ $# -lt 7 ]] ; then
          echo -n "Title: "
          read TITLE
          echo -n "Author: "
          read AUTHOR
          echo -n "LCCN: "
          read LCCN
          echo -n "Copyright: "
          read COPYRIGHT
          echo -n "Publisher: "
          read PUBLISHER
          echo -n "ISBN: "
          read ISBN
          echo -n "Location: "
          read 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=$LIBRARYDIR 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}" \
             ${LIBRARYFILE}
      do_git add `basename ${LIBRARYFILE}`
      do_git commit -m "Added record for \"${TITLE}\""
  }

WORKING Add Books in Bulk

  function bulk_add {
      if [[ $@ -lt 1 ]] ; then
          echo "library bulk-add number"
          exit 1
      fi
      GITOLD=${GIT}
      GIT=FALSE
      for i in {1..$1} ; do
          echo "Adding book number ${i}"
          add_single
      done
      GIT=${GITOLD}
      do_git add `basename ${LIBRARYFILE}`
      do_git commit -m "Added ${1} records"
  }

WORKING Handle Reporting

  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 ${LIBRARYDIRECTORY}/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 -*-" > ${LIBRARYDIRECTORY}/reports/${REPORT}.report
              emacsclient --alternate-editor="" -n ${LIBRARYDIRECTORY}/reports/${REPORT}.report
          ;;
          ,*)
              if [[ -e ${LIBRARYDIRECTORY}/reports/${NAME}.report ]] ; then
                 sh ${LIBRARYDIRECTORY}/reports/${NAME}.report $@
              fi
      esac
  }

WORKING Edit Field

  function do_edit {
      if [[ $# -lt 2 ]] ; then
          echo "ledger edit id field [ value ]"
          exit 1
      fi
      ID=$1
      shift
      FIELD=$1
      shift

      if [[ $FIELD = "Withdrawn" ]] ; then
          TMPDIR=$LIBRARYDIR recset -e "ID = ${ID}" \
                 -f "Withdrawn" -S "`date +"%a, %d %b %Y %H:%M:%S %z"`" \
                 ${LIBRARYFILE}
          TMPDIR=$LIBRARYDIR recset -e "ID = ${ID}" \
                -f "Location" -S "WITHDRAWN" \
                ${LIBRARYFILE}
      else
          VALUE=$1
          shift
          TMPDIR=$LIBRARYDIR recset -e "ID = ${ID}" \
                 -f "${FIELD}" -s "${VALUE}" \
                 ${LIBRARYFILE}
      fi
      do_git add `basename ${LIBRARYFILE}`
      do_git commit -m "Edited record id ${ID}"
  }

  function do_edit_exp {
      if [[ $# -lt 3 ]] ; then
          echo "ledger edit-matching match-exp field value [ commit-message  ]"
          exit 1
      fi
      MATCHEXPRESSION=$1
      shift
      FIELDNAME=$1
      shift
      VALUE=$1
      shift
      TMPDIR=$LIBRARYDIR recset -e "${MATCHEXPRESSION}" \
             -f "${FIELDNAME}" -S "${VALUE}" \
             ${LIBRARYFILE}
      do_git add $(basename ${LIBRARYFILE})
      if [[ $1 != "" ]] ; then
          do_git commit -m "${1}"
      else
          do_git commit -m "Bulk edited records"
      fi
  }

WORKING Loan

  function do_loan {
      if [[ $# -lt 2 ]] ; then
          echo "library loan id name"
          exit 1
      fi

      ID=$1
      shift
      NAME=$1
      shift

      TMPDIR=$LIBRARYDIR recset -e "ID = ${ID}" \
             -f "LoanTo" -S "${NAME}" \
             ${LIBRARYFILE}
      TMPDIR=$LIBRARYDIR recset -e "ID = ${ID}" \
             -f "LoanOn" -S "`date +"%a, %d %b %Y %H:%M:%S %z"`" \
             ${LIBRARYFILE}
      do_git add `basename ${LIBRARYFILE}`
      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=$LIBRARYDIR recset -e "ID = ${ID}" \
             -f "LoanTo" -d \
             ${LIBRARYFILE}
      TMPDIR=$LIBRARYDIR recset -e "ID = ${ID}" \
             -f "LoanOn" -d \
             ${LIBRARYFILE}

      do_git add `basename ${LIBRARYFILE}`
      do_git commit -m "Returned Book ${ID}"
  }

Help Message

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 | 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
  init:       Initialize the database.
  EOF
  }

Packaging

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.

  LIBRARYFILE=~/.library/library.rec
  LIBRARYDIRECTORY=~/.library
  GIT=detect

  <<help-message>>

  <<handle-reports>>

  <<handle-git>>

  <<handle-query>>

  <<add-book>>

  <<add-in-bulk>>

  <<edit-field>>

  <<initialize-database>>

  <<loan>>

  <<process-commands>>