写一个一般的多项选择的小程序,该程序从一个数据文件中读入问题,显示这些问题给用户,程序还能识别正确选项的编号和错误选项的编号。用户可以随时提交答案并计算得分,然后退出程序。
---- 每个主题都在测验的主目录中对应着一个子目录。主目录的名字存放在环境变量 QUIZDIR 中,该变量的默认值为 ~/games/quiz 。这里给出测试的例子中主目录下含有 art 、 politics 、 engineer 三个子目录 ( 对应于三个主题 ) 。
---- 每个主题下包含一系列问题,每个问题在子目录中以文件的形式存在。
---- 每个问题的第 1 行是提问文字。如果这段文字占有多行,那么在新行前必须加一个反斜杠。第 2 行是一个整数,用来表示该问题对应的选项个数。接下去的若干行就是各种选项。最后给出正确答案。
---- 程序在一个主题目录中显示出所有的问题,无论何时用户都可以通过按下 CONTROL+C 组合键退出检测,退出的同时程序将计算测验的结果。在回答完所有问题后程序将计算结果并退出。
---- 程序每次显示一个主题的所有问题之前将打乱显示的顺序。
写在前面的说明:
games 主目录下不仅含有 3 个子目录作为供选择的主题,还含有文件名为 quiz 的 shell 脚本程序,该脚本是考试小程序运行程序。
在 art 主题目录下含有文件 mod 和脚本文件 setques.sh ,这两个文件用来在该主题下生成测试用的问题文件,运行 setques.sh 传入的参数是需要生成的问题文件数。
给出完整的测试流程和 shell 代码本身:
>cd ~
>mkdir -p games/quiz
>cd quiz
>mkdir engineer art politics
>cat -n quiz
1 #!/bin/bash
2
3 # remove the #on the following line to turn on debugging
4 #set -o xtrace
5
6 #==============
7 function initialize()
8 {
9 trap 'summarize ; exit 0' INT
10 num_ques=0
11 num_correct=0
12 first_time=true
13 cd ${QUIZDIR:=~/games/quiz} || exit 2
14 }
初始化,将变量 QUIZDIR 的值设置为脚本程序的工作目录,如果没有设置该变量,那么默认就为 ~/games/quiz ;设置捕获 CONTROL+C 信号的信号捕捉命令,即响应 CONTROL+C 信号的处理命令是 'summarize;exit 0' 。
15
16 #=============
17 function choose_subj()
18 {
19 subjects=($(ls))
20 PS3="Choose a subject for the quiz from the preceding list:"
21 select Subject in ${subjects[*]} ;do
22 if [[ -z "$Subject" ]]
23 then
24 echo "No subject chosen.Bye" ?>2
25 exit 1
26 fi
27 echo $Subject
28 return 0
29 done
30 }
选择主题,利用命令 'ls' 和命令替换 $(ls) ,的输出作为元素 --($(ls))-- 对数组 subjects 赋值;利用 select 结构给用户显示主题名字,并把结果保存到变量 Subject 中,并把主题目录名字写到标准输出上 ( 注意主程序将利用命令替换来使用该输出,作为赋值表达式的右值 ) 。命令替换:利用其中命令的输出来代替整条命令。
31
32 #============
33 function exchange()
34 {
35 temp_value=${questions[$1]}
36 questions[$1]=${questions[$2]}
37 questions[$2]=$temp_value
38 }
39
40 #===========
41 function scramble()
42 {
43 typeset -i index quescount
44 questions=($(ls))
45 quescount=${#questions[*]}
46 ((index=quescount-1))
47 while [[ $index >0 ]];do
48 ((target=RANDOM % index ))
49 exchange $target $index
50 ((index -= 1))
51 done
52 }
数组变量 questions 保存所有问题的名字,注意函数中定义的变量具有全局性,除非利用 typeset 声明为局部变量,所以在 exchange 函数中可以对 questions 中的数组元素进行交换操作。同样对 questions 的赋值也是利用命令替换来完成的; quescount 保存了数组元素的个数;函数让索引变量 index 从 quescount 开始向下计数。对索引的每个值,函数选择一个介于 0~index 之间的随机值 ( 取模操作将随机数对应到 0 ~ index 范围 ) ,然后将索引值和选择的随机值所对应数组位置的元素进行对换。运行 scramble 函数的结果会得到两个全局性的变量 questions( 数组,元素为主题目录下的所有文件名,元素与文件名的对应关系被随机排列,注意一个文件对应着一个问题 ) 和 quescount( 数组元素的个数也就是问题的个数 ) 。由于函数运行环境通常与被其调用的环境相同,所以其中的变量是显示被函数和调用它的 shell 共享。
53
54 #===========
55 function ask()
56 {
57 exec 3<$1
58 read -u3 ques || exit 2
59 read -u3 num_opts || exit 2
60 index=0
61 choices=()
62 while (( index < num_opts));do
63 read -u3 next_choice || exit 2
64 choices=("${choices[@]}" "$next_choice")
65 ((index += 1))
66 done
67 read -u3 correct_answer || exit 2
68 exec 3<&-
69
70 if [[ $first_time = true ]];then
71 first_time=false
72 echo -e "You may press the interrupt key at any time to quit./n"
73 fi
74
75 PS3="$ques"
76
77 select answer in "${choices[@]}" ;do
78 if [[ -z "$answer" ]];then
79 echo Not a valid choice.Please choose again.
80 elif [[ "$answer" = "$correct_answer" ]];then
81 echo "Correct!"
82 return 1
83 else
84 echo "No,the answer is $correct_answer."
85 return 0
86 fi
87 done
88 }
ask 函数从参数中读入问题文件,并使用文件的内容显示问题,接受用户输入的答案,同时还要判断答案的正确性,函数参数为问题文件的文件名。
利用文件描述符 3 打开参数 ($1- 问题文件文件名 ) 文件;从其中读入相应问题到变量 ques 中,读入选项到数组 choices 中,利用 select 结构显示选项并接受问题答案到变量 answer 中,利用 select 结构的显示提示符 PS3 显示问题 ques 。
89
90 #===========
91 function summarize()
92 {
93 echo
94 if (( num_ques == 0 ));then
95 echo "You did not answer any questions"
96 exit 0
97 fi
98
99 ((percent=num_correct*100/num_ques))
100 echo "You answered $num_correct questions correctly,out of /
101 $num_ques total questions."
102 echo "Your score is $percent percent."
103 }
summarize 函数为最后统计结果函数。注意在用户测试的过程中利用 CONTROL+C 退出程序时也将调用该函数,因为初始化利用 trap 命令将其信号处理命令定义为了 'summarize;exit 0' ;注意函数定义变量的全局性 ( 在定义这些函数的 shell 环境中全局有效 ) ,正是因为如此才可顺利的显示测验结果。
104
105 #==========
106 initialize
107
108 subject=$(choose_subj)
109 [[ $? -eq 0 ]] || exit 2
110
111 cd $subject || exit 2
112 echo
113 scramble
114
115 for ques in ${questions[*]};do
116 ask $ques
117 result=$?
118 (( num_ques=num_ques+1 ))
119 if [[ $result == 1 ]];then
120 (( num_correct += 1 ))
121 fi
122 echo
123 sleep ${QUIZDELAY:=1}
124 done
125
126 summarize
127 exit 0
128
quiz 主程序,初始化定义一些变量并设置 INT 信号处理命令;运行 choose_subj 命令替换,导致其输出赋值给 subject ,也即是选择的主题目录;利用 cd 命令进入所选择的主题目录;利用函数 scramble 得到主题目录下所有问题文件名的数组 (questions) 和问题文件数 (quescount)( 一个问题文件对应一个问题,利用函数变量的全局性在主程序中使用这两个变量 ) ;利用 for 结构遍历问题文件名数组中的每个文件名,利用函数 ask( 参数为问题文件的文件名 ) 显示问题并得到答案, ask 函数的运行结果 ( 返回值 ) 保存到变量 result 中以便统计测验结果。
>cd art
>cat -n mod
1 4
2 0
3 1
4 2
5 3
>cat -n setques.sh
1 #!/bin/bash
2
3 index=0
4 answer=
5 while [ $index -lt $1 ]
6 do
7 echo "The question is $index:">temp
8 # echo -e "The question is $index:/n">temp
9 cat temp mod > $$.$index
10 # echo -e "/n" >>$$.$index
11 answer=$(( $index & 3))
12 echo $answer >>$$.$index
13 (( index+=1))
14 done
15 rm temp
脚本 setques.sh 用来产生测试用问题文件。问题名,选项,答案!下面是运行后的一个问题文件示例:
>./setques.sh 10
>ls
>cat -n 2868.6
1 The question is 6:
2 4
3 0
4 1
5 2
6 3
7 2
>cd ..
>./quiz
对 setques.sh 可以完善,例如加入对信号的捕捉,使其利用中断键等形式退出时也能够删除 temp 文件。