闇の光

读书笔记 经验感受

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

CD database application

既然我们已经学习了shell程序语言的主要特性,现在就让我们用我们所学的东西来创建一个简单完整的程序CD database application。

要求:

首先,要存储CD的一些基本信息,比如:标签、音乐类型以及艺术家或作曲家。同时,还要储存一些简单的音轨信息。之后,我们的CD信息搜索是根据每一片CD的信息来搜索,而不是根据音轨的具体信息来搜索。最后,我们还要在此程序中加入进入、更新以及删除相关的信息的功能。

设计:

三个要求——更新、搜索、显示数据只需要一个简单的菜单就足够了。所有的数据都以文本的方式存储,当然这是假设我们的CD数量不是太多的情况,我们没必要设计一个复杂的数据库。而且,存储信息在文本中,除了能够让程序简单之外,在你的需求改变时,文本文件相对于其他文件格式会更显得容易操作些。比如,你甚至可以通过编辑器来打开文本并修改数据,而无需设计专门的软件来做这件事。

关于数据存储我们需要做一些重要的设计决定:一个文件是否足够?如果够的话,文本显示格式如何设计?还有就是除了每张CD的音轨信息之外有关于CD本身所要记录的信息,本程序只考虑一片CD的艺术家或作曲家只有一位。

如果我们要求可以灵活地存贮音轨数目,有三种选择:

  • 1. 只使用一个文件,有一行用来存储CD标题"title"信息,之后的n行用来记录此CD的音轨信息。
  • 2. 将每片CD的所有信息都只记录在一行中。
  • 3. 将CD标题信息与音轨信息分开记录在不同的文件中。
  • 上面只有第三种可以让我们很容易的来确定文件的显示格式,且这种文件格式可以让我们以后能够将这些信息转换为一种相互关联的表单。因此,我们选择第三种。

    下面就该决定信息将如何放入文件中。

    首先,对于每一片CD的标题,我们将如下分类:

  • The CD catalog number
  • The title
  • The type(classical, rock, jazz, etc.)
  • The composer or artist
  • 而对于音轨,我们只简单的分为两类:

  • Track number
  • Track name
  • 为了连接这两个文件,我们必须将CD音轨的信息同CD的其他信息相互关联。因此,我们给每片CD设置一个商品编号。该编号对每一片CD都是唯一的,它在标题文件只出现一次,在音轨文件的每支音轨的信息中也只出现一次。

    下面是大概的显示效果:

    标题文件
    CatalogTitleTypeComposer
    CD123Cool saxJazzBix
    CD234Classic violinClassicalBach
    CD345Hits99PopVarious
    音轨文件
    CatalogTrack No.Title
    CD1231Some jazz
    CD1232More jazz
    CD2341Sonata in D minor
    CD3451Dizzy

    由此我们可知这两个文件之间是通过Catalog来相互连接的。

    最后一件我们需要决定的事就是怎样来隔离这些输入项。通常在一个关系数据库中是通过固定宽度的字段来隔离这些输入项,不过这显然在此不够方便。还有另外一种普遍的方法,那就是使用逗号来分隔,我们在此所使用的就是这种方法。

    下面是我们在程序中所使用到的自定义函数:

    get_return()
    get_confirm()
    set_menu_choice()
    insert_title()
    insert_track()
    add_record_tracks()
    add_records()
    find_cd()
    update_cd()
    count_cds()
    remove_records()
    list_tracks()

    实现过程

    1.首先,当然是每个shell脚本都要进行的操作,声明这是一个shell脚本,在此之后是一些相关的版权信息。此处,偷懒就不写什么版权信息了。

    #!/bin/bash

    2.接下来就是设置全局变量。设置标题文件、音轨文件、一个临时文件以及设置当用户中断脚本(Ctrl+C),删除所设置的临时文件。

    menu_choice=""
    current_cd=""
    title_file="title.cdb"
    tracks_file="tracks.cdb"
    temp_file=/tmp/cdb.$$
    trap 'rm -f $temp_file' EXIT

    3.现在定义函数,由于脚本都是从上往下执行,因此我们需要在函数被调用前将其定义好。为了避免在脚本中多次书写相同的代码,我们先定义这两个简单的函数:

    get_return() {
       echo -e "Press return \c"
       read x
       return 0
    }

    get_confirm() {
       echo -e "Are you sure? \c"
       while true
       do
          read x
          case "$x" in
              y | yes | Y | Yes | YES )
                 return 0;;
              n | no | N | No | NO )
                 echo
                 echo "Cancelled"
                 return 1;;
              * ) echo "Please enter yes or no" ;;
          esac
        done
    }

    4.接下来是程序的主菜单函数set_menu_choice,菜单的内容是动态变化的,如果一个CD条目入口被选择后,将出现一些额外的选项。

    set_menu_choice() {
       clear
       echo "Options : -"
       echo
       echo "   a) Add new CD"
       echo "   f) Find CD"
       echo "   c) Count the CDs and tracks in the catalog"
       if [ "$cdcatnum" != "" ]; then
          echo "   l) List tracks on $cdtitle"
          echo "   r) Remove $cdtitle"
          echo "   u) Update track information for $cdtitle"
       fi
       echo "   q) Quit"
       echo
       echo -e "Please enter choice then press return \c"
       read menu_choice
       return
    }

    5.之后是两个比较短的函数inset_title和insert_track,它们的功能是用来添加数据文件。在它们之后便是调用它们的函数add_record_track。这个函数通过模式匹配来确保没有逗号被输入到数据文件中(因为我们使用逗号作为分隔符),以及当音轨被输入是通过算术运算来增加当前音轨数目:

    insert_title() {
       echo $* >> $title_file
       return
    }

    insert_track() {
       echo $* >> $tracks_file
       return
    }

    add_record_tracks() {
       echo "Enter track information for this CD"
       echo "When no more tracks enter q"
       cdtrack=1
       cdttitle=""
       while [ "$cdttitle" != "q" ]
       do
          echo -e "Track $cdtrack, track title? \c"
          read tmp
          cdttitle=${tmp%%,*}
          if [ "$tmp" != "$cdttitle" ]; then
              echo "Sorry, no commas allowed"
              continue
          fi
          if [ -n "$cdttitle" ]; then
              if [ "$cdttitle" != "q" ]; then
                insert_track $cdcatnum, $cdtrack, $cdttitle
              fi
          else
              cdtrack=$((cdtrack-1))
          fi
          cdtrack=$((cdtrack+1))
       done
    }

    此函数中的cdttitle=${tmp%%,*}所代表的意思从结尾开始匹配最大以逗号开始后接任意字符的字符串,并输出其剩余部分。即:假设tmp=111,222,333,444 ,则cdttitle=111。相关内容请查看Parameter Expansion。

    6.接下来的函数add_records:

    add_records(){
      # Prompt for the initial information

       echo -e "Enter catalog name \c"
       read tmp
       cdcatnum=${tmp%%,*}

       echo -e "Enter title \c"
       read tmp
       cdtitle=${tmp%%,*}

       echo -e "Enter type \c"
       read tmp
       cdtype=${tmp%%,*}

       echo -e "Enter artist/composer \c"
       read tmp
       cdac=${tmp%%,*}

       # Check that they want to enter the information

       echo About to add new entry
       echo "$cdcatnum $cdtitle $cdtype $cdac"

       # If confirmed then append it to the titles file

       if get_confirm ; then
         insert_title $cdcatnum, $cdtitle, $cdtype, $cdac
         add_record_tracks
       else
         remove_records
       fi

       return
    }

    7.下面的函数find_cd通过使用grep命令在CD标题文件中搜索Catalog名称。我们需要知道被搜索的字段被发现的次数,但grep只返回一个值来显示它被匹配零次或更多次。为了得到具体的数值,我们将输出导入到一个文件中,每匹配一次就导入一行,之后再来计算此文件中的行数。

    命令wc在它的输出中以空格来分隔结果,结果分别是文件中的行数、单词数以及字符数。因此我们通过使用$(wc -l $temp_file)来从输出中提取第一参数,即文件的行数,用来赋值给变量linesfound。如果你想要后面其他的参数,可以使用set命令设置命令wc输出的相关shell的参数变量。

    更改IFS(Internal Field Separator)为逗号,这样我们才可以分隔以逗号来定界的字段。还有一个可供替代的选择,就是使用命令cut。

    find_cd() {
       if [ "$1" = "n" ]; then
         asklist=n
       else
         aslist=y
       fi
       cdcatnum=""
       echo -e "Enter a string to search for in the CD titles \c"
       read searchstr
       if [ "$searchstr" = "" ]; then
         retrun 0
       fi

       grep "$searchstr" $title_file > $temp_file

       set $(wc -l $temp_file)
       linesfound=$1

       case "$linesfound" in
          0)  echo "Sorry, nothing found"
              get_return
              return 0
              ;;
          1)  ;;
          2)  echo "Sorry, not unique."
              echo "Found the following"
              cat $temp_file
              get_return
              return 0
       esac
       
       IFS=","
       read cdcatnum cdtitle cdtype cdac < $temp_file
       IFS=" "

       if [ -z "$cdcatnum" ]; then
         echo "Sorry, could not extract catalog field from $temp_file"
         get_return
         return 0
       fi

       echo
       echo Catalog number: $cdcatnum
       echo Title: $cdtitle
       echo Type: $cdtype
       echo Artist/Composer: $cdac
       echo
       get_return

       if [ "$asklist" = "y" ]; then
         echo -e "View tracks for this CD? \c"
           read x
         if [ "$x" = "y" ]; then
           echo
           list_tracks
           echo
         fi
       fi
       return 1
    }

    8.接下来是函数update_cd:

    update_cd() {
       if [ -z "$cdcatnum" ]; then
         echo "You must select a CD first"
         find_cd n
       fi
       if [ -n "$cdcatnum" ]; then
         echo "Current tracks are :-"
         list_tracks
         echo
         echo "This will re-enter the tracks for $cdtitle"
         get_confirm && {
           grep -v "^${cdcanum}," $tracks_file > $temp_file
           mv $temp_file $tracks_file
           echo
           add_record_tracks
         }
       fi
       retrun
    }

    9.在之后是函数count_cds:

    count_cds() {
       set $(wc -l $title_file)
       num_titles=$1
       set $(wc -l $tracks_file)
       num_tracks=$1
       echo found $num_titles CDs, with a total of $num_tracks tracks
       get_return
       return
    }

    10.接下来是函数remove_records,将输入条目从数据文件中剔除,我们使用grep -v来移除所有匹配的字符串。需要注意的是我们必须使用一个临时文件来完成此项操作,如果你这样使用:grep -v "^$cdcatnum" > $title_file,标题文件$title_file将在grep执行更改前被>输出重定向为空,因此grep所读取的是一个空的文件。

    remove_records() {
       if [ -z "$cdcatnum" ]; then
         echo You must select a CD first
         find_cd n
       fi
       if [ -n "$cdcatnum" ]; then
         echo "You are about to delete $cdtitle"
         get_confirm && {
           grep -v "^${cdcatnum}," $title_file > $temp_file
           mv $temp_file $title_file
           grep -v "^${cdcatnum}," $tracks_file > $temp_file
           mv $temp_file $tracks_file
           cdcatnum=""
           echo Entry removed
         }
         get_return
       fi
       return
    }

    11.之后是函数list_tracks,在其内我们使用grep来提取我们所要的行数,cut来访问我们所想要的字段,而之后的more则提供一个标记页码的输出:

    list_tracks() {
       if [ "$cdcatnum" = "" ]; then
         echo no CD selected yet
         return
       else
         grep "^${cdcatnum}," $tracks_file > $temp_file
         num_tracks=$(wc -l $temp_file)
         if [ "$num_tracks" = "0" ]; then
           echo no tracks found for $cdtitle
         else {
           echo
           echo "$cdtitle :-"
           echo
           cut -f 2- -d , $temp_file
           echo
         } | ${PAGER:-more}
         fi
       fi
       get_return
       return
    }

    12.OK,现在所有的函数都已经定义好了,接下来我们开始书写脚本的主体部分。首先在前面几行我们先得到两个数据存储文件的状况,之后调用菜单函数set_menu_choice,并根据输入做出相应的动作。当quit被选中后,删除临时文件,同时输出“Finished”信息,最后退出脚本:

    rm -f $temp_file
    if [ ! -f $title_file ]; then
       touch $title_file
    fi
    if [ ! -f $tracks_file ]; then
       touch $tracks_file
    fi

    # Now the application proper

    clear
    echo
    echo
    echo "Mini CD manager"
    sleep 1

    quit=n
    while [ "$quit" != "y" ];
    do
       set_menu_choice
       case "$menu_choice" in
         a )  add_records;;
         r )  remove_records;;
         f )  find_cd y;;
         u )  update_cd;;
         c )  count_cds;;
         l)   list_tracks;;
         b)   echo
             more $title_file
             echo
             get_return;;
         q | Q ) quit=y;;
         * )  echo "Sorry, choice not recognized";;
       esac
    done

    # Tidy up and leave

    rm -f $temp_file
    echo "Finished"
    exit 0

    注意事项

    在脚本一开始的trap命令,它的功效就相当于用户按下Ctrl+C,不过要注意的是,我们所要使用的信号可能为EXIT或INT,而这取决于终端本身的设置。除此之外,我们在实现菜单选择时,还可以在bash和ksh中使用select结构来完成。此结构是一个专用的菜单选择器,不过不利于脚本移植。

    posted on 2008-03-27 15:00  taizi  阅读(1123)  评论(0编辑  收藏  举报