CD database application
既然我们已经学习了shell程序语言的主要特性,现在就让我们用我们所学的东西来创建一个简单完整的程序CD database application。
要求:
首先,要存储CD的一些基本信息,比如:标签、音乐类型以及艺术家或作曲家。同时,还要储存一些简单的音轨信息。之后,我们的CD信息搜索是根据每一片CD的信息来搜索,而不是根据音轨的具体信息来搜索。最后,我们还要在此程序中加入进入、更新以及删除相关的信息的功能。
设计:
三个要求——更新、搜索、显示数据只需要一个简单的菜单就足够了。所有的数据都以文本的方式存储,当然这是假设我们的CD数量不是太多的情况,我们没必要设计一个复杂的数据库。而且,存储信息在文本中,除了能够让程序简单之外,在你的需求改变时,文本文件相对于其他文件格式会更显得容易操作些。比如,你甚至可以通过编辑器来打开文本并修改数据,而无需设计专门的软件来做这件事。
关于数据存储我们需要做一些重要的设计决定:一个文件是否足够?如果够的话,文本显示格式如何设计?还有就是除了每张CD的音轨信息之外有关于CD本身所要记录的信息,本程序只考虑一片CD的艺术家或作曲家只有一位。
如果我们要求可以灵活地存贮音轨数目,有三种选择:
上面只有第三种可以让我们很容易的来确定文件的显示格式,且这种文件格式可以让我们以后能够将这些信息转换为一种相互关联的表单。因此,我们选择第三种。
下面就该决定信息将如何放入文件中。
首先,对于每一片CD的标题,我们将如下分类:
而对于音轨,我们只简单的分为两类:
为了连接这两个文件,我们必须将CD音轨的信息同CD的其他信息相互关联。因此,我们给每片CD设置一个商品编号。该编号对每一片CD都是唯一的,它在标题文件只出现一次,在音轨文件的每支音轨的信息中也只出现一次。
下面是大概的显示效果:
标题文件Catalog | Title | Type | Composer |
CD123 | Cool sax | Jazz | Bix |
CD234 | Classic violin | Classical | Bach |
CD345 | Hits99 | Pop | Various |
Catalog | Track No. | Title |
CD123 | 1 | Some jazz |
CD123 | 2 | More jazz |
CD234 | 1 | Sonata in D minor |
CD345 | 1 | Dizzy |
由此我们可知这两个文件之间是通过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结构来完成。此结构是一个专用的菜单选择器,不过不利于脚本移植。