标签为 Shell 的文章

Shell脚本之并发编程

在Python中,有很多模块都可以实现并发编程,比如 threading, processing, eventlet 与 Stackless Python 等。

那么对于Shell而言,又如何实现呢?其实原理很简单,我采用的方法是:
1. 将需要执行的任务分批放入后台执行;
2. 将后台执行的命令结果汇总到指定的文件中;
3. 使用wait命令来等待所有任务执行结束。

下面的脚本就用到了这样的并发编程方法,实现的功能是:
快速(3-4秒内)对相同C网内的所有IP(255个)通过命令ping进行测试并返回结果。

vim fastping.sh

#!/bin/bash 

# default settings
subnet=$1 # C type subnet
retry=2 # retry times
timeout=3 # timeout seconds
output=/tmp/ping.output # output file

# function print_help
function print_help(){
  echo "Examples:"
  echo ${0} 172.17.32
  echo ${0} 192.168.1 unable
  exit 1
}

# check the parameter
if [ $# -lt 1 ]; then
  print_help
fi

# check the network parameter's format
count=0
for i in $(echo $1 |sed 's/\./ /g')
do
  count=$((${count}+1))
done
if [ ${count} -ne 3 ]; then
  print_help
fi

# clean the output file
> ${output}

function pingable(){
  ping -c ${retry} -w ${timeout} -q ${subnet}.${i} &> /dev/null && echo ${i} >> ${output}
}

function unpingable(){
  ping -c ${retry} -w ${timeout} -q ${subnet}.${i} &> /dev/null || echo ${i} >> ${output}
}

# get the check type
if [ "$2" == "unable" ]; then
  status="unpingable"
else
  status="pingable"
fi

# ping as paraller mode and write output into file
for i in {1..255}
do 
  ${status} &
done

# wait for all ping processes done
wait

# print output with better order
sum=$(wc -l ${output} |awk '{print $1}')
echo "There are '${sum}' '${status}' IPs begin with '${subnet}.' :"
cat ${output} |sort |xargs -n 20 echo " "

chmod +x fastping.sh
./fastping.sh

Examples:
./fastping 172.17.32
./fastping 192.168.1 unable

time ./fastping.sh 192.168.1

There are '142' 'pingable' IPs begin with '192.168.1' :
  1 10 12 13 14 15 16 18 19 20 21 22 23 24 25 26 27 28 29 30
  31 32 33 34 35 36 37 38 40 41 42 43 44 45 46 47 48 49 50 51
  52 53 54 55 56 57 59 60 61 62 63 64 65 66 67 68 69 70 71 72
  73 74 75 76 77 78 80 81 83 84 85 86 87 88 89 90 91 92 93 94
  95 96 97 98 99 100 101 102 103 104 105 106 107 108 112 113 114 115 116 117
  118 119 120 121 122 123 124 125 126 127 128 133 134 135 136 137 138 139 140 141
  142 143 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
  170 254

real    0m3.201s
user    0m0.135s
sys     0m0.495s

No Comments

简单实现自动过滤WEB攻击源IP

最近VPS负载经常会比较高,刚刚仔细检查了一下,发现有一些IP在非常频繁的向Blog发请求,大多数是关于评论和后台登陆页面的。
由此造成了fastcgi的进程非常繁忙,使得VPS本身就非常弱小的CPU一直处于高利用率的状态。
于是,我编写了一个简单的脚本,每隔1分钟从服务器日志中取出这些IP并通过iptables将其直接Block掉,用起来效果很不错。

根据我目前服务器的访问日志来看,如果在10000次请求中,有500次请求来自于同一个IP的话,那么其攻击特征就非常明显了。
另外,针对后台登陆页面和评论页面,我单独制定了更加严格的规则,那就是在10000次POST请求中,如果有50次尝试,就Block掉。

具体的脚本内容如下:
$ vim /home/rainbow/sbin/block_attack_ips.sh

#!/bin/bash

logfiles=(
/webserver/blog/logs/rainbow_access.log
/webserver/blog/logs/eric_access.log
)

whitelist=$(last | awk '{print $3}' | grep ^[1-9] | sort | uniq | xargs)
whitelist+=" 127.0.0.1 172.31.23.107 52.69.213.155"

function check_root(){
  if [ $EUID -ne 0 ]; then
    echo "This script must be run as root"
    exit 1
  fi
}

function block_ips(){
  blacklist=$@
  if [ ! -z "${blacklist}" ]; then
    for ip in ${blacklist}
    do
      if ! $(echo ${whitelist} | grep -wq ${ip}); then
        if ! $(/sbin/iptables-save | grep -wq ${ip}); then
          echo "Blocked ${ip}"
          /sbin/iptables -I INPUT -s ${ip}/32 -p tcp -m tcp --dport 80 -j DROP
        fi
      fi
    done
  fi
}

function check_post(){
  page=$1
  tailnum=$2
  retry=$3

  command="grep -w POST ${logfile} |tail -n ${tailnum} |grep -w ${page} |awk '{print \$1}' |sort |uniq -c |awk '(\$1 > ${retry}){print \$2}'"
  blacklist=$(eval ${command})
  block_ips ${blacklist}
}

function check_all(){
  tailnum=$1
  retry=$2

  command="tail -n ${tailnum} ${logfile} |awk '{print \$1}' |sort |uniq -c |awk '(\$1 > ${retry}){print \$2}'"
  blacklist=$(eval ${command})
  block_ips ${blacklist}
}

check_root
for logfile in ${logfiles[@]}
do
  check_post wp-login.php 10000 50
  check_post wp-comments-post.php 10000 50
  check_all 10000 500
done

$ chmod +x /home/rainbow/sbin/block_attack_ips.sh

配置crontab计划任务,每1分钟检查一次,并每月定时重启iptables服务清除旧的记录:
$ sudo crontab -e

*/1 * * * * /home/rainbow/sbin/block_attack_ips.sh
00 01 1 * * /etc/init.d/iptables restart

No Comments

分享一个用于管理Vsftp的虚拟用户的脚本[原创实践]

由于公司的一台服务器上使用FTP的用户越来越多,为了方便管理和安全考虑,我们将这些用户账号从之前的操作系统用户迁移到了虚拟用户。
但也因此无法像以前对系统用户那样很方便的对虚拟用户进行添加,删除,禁用以及修改密码等操作了,为了解决这个问题,我自己编写了一个Shell脚本,用它可以实现对虚拟用户的的创建,删除,禁用,激活以及修改密码的操作,用起来感觉不错,再次特分享给大家。

首先,关于Vsftp的虚拟用户方式的安装与配置,可以参考:http://heylinux.com/archives/726.html

在以虚拟用户方式配置好Vsftp以后,就可以通过以下方式使用脚本了:
创建用户:./user.vsftpd.sh create jack
删除用户:./user.vsftpd.sh delete jack
禁用用户:./user.vsftpd.sh disable jack
激活用户:./user.vsftpd.sh enable jack
修改密码:./user.vsftpd.sh passwd jack

创建脚本,输入以下内容:
# vim user.vsftpd.sh
# chmod +x user.vsftpd.sh

#!/bin/bash
#The script can create,deactivate,activate and delete virtual users of vsftpd.
#Author: Dong Guo
#Date: December 12 2011

USERFILE=/etc/vsftpd/virtusers
USERDB=/etc/vsftpd/virtusers.db
CONFBASE=/etc/vsftpd/vconf
TMPCONF=/etc/vsftpd/vconf/vconf.tmp
FTPBASE=/ftphome
FTPHOST=ftphost
USERNAME=$2

if [ $# != 2 ];then
        echo "Usage: $0 {create|disable|enable|passwd|delete} {username}" >&2
        exit 1
fi

function check_username_exist() {
                #Check if virtual user already exist
                USERCOUNT=$(sed -n 'p;n' $USERFILE | grep -w $USERNAME | wc -l)
                if [ $USERCOUNT -ne 0 ];then
                echo "User $USERNAME ALREADY exist!" && exit
                fi
}

check_username_notexist() {
                #Check if virtual user not exist
                USERCOUNT=$(sed -n 'p;n' $USERFILE | grep -w $USERNAME | wc -l)
                if [ $USERCOUNT -eq 0 ];then
                echo "User $USERNAME NOT exist!" && exit
                fi
}

get_password() {
                #Get the password
                echo -n "Input password: "
                read password
                #Check if password is empty
                if [ -z "$password" ];then
                echo "Empty password!!" && exit
                fi
}

update_userdb() {
                #Delete the virtual user db
                rm -f $USERDB
                #Generate the virtual user db
                db42_load -T -t hash -f $USERFILE $USERDB
}

case "$1" in
        'create' )
                check_username_exist
                get_password
                #Write the username and password to $USERFILE
                echo $USERNAME >> $USERFILE
                echo $password >> $USERFILE
                update_userdb
                #Create the configure file of virtual user
                cp $TMPCONF $CONFBASE/$USERNAME
                #Replace the home directory name of virtual user
                sed -i "s/virtuser/$USERNAME/g" $CONFBASE/$USERNAME
                #Create the home directory of virtual user
                mkdir $FTPBASE/$USERNAME
                #Change the owner of home directory to OS user $FTPHOST
                chown -R $FTPHOST:$FTPHOST $FTPBASE/$USERNAME
                ;;

        'disable' )
                check_username_notexist
                #Change the owner of home directory from $FTPHOST to root
                chown root:root $FTPBASE/$USERNAME
                #Change the permissions of home directory to read-only for root
                chmod 700 $FTPBASE/$USERNAME
                ;;

        'enable' )
                check_username_notexist
                #Change the owner of home directory from root to $FTPHOST to root
                chown $FTPHOST:$FTPHOST $FTPBASE/$USERNAME
                #Change the permissions of home directory to 775 for $FTPHOST
                chmod 775 $FTPBASE/$USERNAME
                ;;

        'delete' )
                check_username_notexist
                #Get the row numbers of username and password of virtual user
                ROWNUMBER=$(cat -n $USERFILE | sed -n 'p;n' | grep -w $USERNAME | awk '{print $1}' | head -n 1)
                #Delete the username and password of virtual user from $USERFILE
                sed -i "${ROWNUMBER}d" $USERFILE
                sed -i "${ROWNUMBER}d" $USERFILE
                update_userdb
                #Delete the configure file of virtual user
                rm -f $CONFBASE/$USERNAME
                #Rename the home directory name of virtual user
                mv $FTPBASE/$USERNAME $FTPBASE/$USERNAME.deleted
                ;;

        'passwd' )
                check_username_notexist
                get_password
                #Get the row numbers of username and password of virtual user
                ROWNUMBER=$(cat -n $USERFILE | sed -n 'p;n' | grep -w $USERNAME | awk '{print $1}' | head -n 1)
                PASSWORDNUMBER=$(expr $ROWNUMBER + 1)
                sed -i "${PASSWORDNUMBER}d" $USERFILE
                sed -i "${ROWNUMBER}a $password" $USERFILE
                update_userdb
                ;;
        *)
                echo "Usage: $0 {create|disable|enable|passwd|delete} {username}" >&2
                exit 1
                ;;
esac

,

2 Comments

贡献一份简单的Linux自动化远程FTP备份的脚本

脚本功能介绍:
每天自动备份本地MySQL数据库,并上传备份文件到远程FTP服务器,传输完成后删除本地一周以前旧的备份文件;

# vim backupdb.sh

#!/bin/bash
cd /data/backup
DATE=`date +"%y%m%d"` #定义时间变量,方便备份文件以时间命名

mysqldump -udbuser -pdbpass dbname > "$DATE".dbname.sql #备份MySQL数据库
tar czvf "$DATE".dbname.tgz "$DATE".dbname.sql #压缩备份出的MySQL数据库dump文件

ftp -n <<!  #开始FTP自动传输
open 192.168.1.10 #连接FTP服务器
user ftpuser ftppass #输入FTP用户名密码
binary #以二进制格式传输
prompt off #关闭交互提示
cd /ftpbackup/mysqldb #进入远程FTP服务器的备份目录
lcd /data/backup #进入本地的备份目录
put "$DATE".dbname.tgz #上传压缩后的MySQL备份文件
bye #退出FTP服务器
!

find /data/backup/*.sql -mtime +7 -delete #删除7天以前的MySQL数据库dump文件
find /data/backup/*.tgz -mtime +7 -delete #删除7天以前的MySQL数据库压缩文件

给脚本加上可执行权限
# chmod +x backupdb.sh

使脚本在每天的凌晨2点执行
#crontab -e

0 2 * * * /root/backupdb.sh

No Comments

推荐一本非常不错的BASH中文电子书教程

这是目前最好的BASH教程,内容全面,详尽无比,有很多脚本实例;最重要的是作者一直在更新和修正此文档。
 
作者是LinuxSir.org上的杨春敏、黄毅,他们经过大半年的努力,终于将这本厚厚的英文书籍(原作者用了6年时间写并更新此书)翻译出来了,在此向这两位译者致敬,感谢你们超乎寻常的辛勤劳动。
 
点击这里可直接下载PDF版电子书:http://www.linuxsir.org/main/doc/abs/ABS_Guide_cn.pdf.tar.bz2
欲了解更多,请点击这里:http://www.linuxsir.org/main/?q=node/140

No Comments

原创SHELL脚本共享:系统实时监控程序(基于TOP指令优化)

TOP相当于Linux下的资源管理器,我们公司有一部分重要的服务器便是通过该命令直接实时监控的。
但是,TOP虽然十分强大,对我们监控却并不是很适合,不能很直观的立刻锁定到重点的数据。更不能利用程序自动完成一些判断了。
因此,我花了一个晚上的时间,完成了下面的这个脚本,只要安装的TOP命令的Linux主机都可以正常使用该脚本!
它目前还是很初级的版本,所以很简单,甚至很弱,但它也实现了以下一些功能:
1.更亲和的人机界面,将重要的数据如CPU,负载,内存百分比等直接输出到平面上;
2.实现了对前一秒的流量统计;
3.更自由的用户自定义选项,可以通过变量来和直接修改脚本来调整排盘和自己的偏好;
4.自动判断功能,当某一项数值出现异常时(依赖于你的预设值),将会现在在顶部有警告信息。
我将会把这一脚本更加的完善,并完成另一份不依赖于TOP指令的脚本(它主要通过常用命令和/proc文件中获取)。
大家任何有需要的可以直接粘贴过去使用,另外,有任何完善的建议的请直接向我提出来,谢谢。

 
#!/bin/bash
#############################################
#### System information listen tool v0.2 ####
####               powered by mcsrainbow ####
#############################################
while true
do
  
  ###Copy the top's content to top.info###
  top -b1 -n1 >top.info
  sleep 2
   
  ###Check the flow###
  typeset in in_old dif_in
  typeset out out_old dif_out
 
  function FLOW(){
  in_flow1=$(cat /proc/net/dev |grep eth0 |sed -e "s/(.*):(.*)/[1]/g" |awk ' { print $1 }' |cut -d: -f2)
  in_flow2=$(cat /proc/net/dev |grep eth1 |sed -e "s/(.*):(.*)/[1]/g" |awk ' { print $1 }' |cut -d: -f2)
  in_flow_byte=$(expr $in_flow1 + $in_flow2)
  in_flow=$(expr $in_flow_byte / 1024)
  out_flow1=$(cat /proc/net/dev |grep eth0 |sed -e "s/(.*):(.*)/[1]/g" |awk ' { print $9 }')
  out_flow2=$(cat /proc/net/dev |grep eth1 |sed -e "s/(.*):(.*)/[1]/g" |awk ' { print $9 }')
  out_flow_byte=$(expr $out_flow1 + $out_flow2)
  out_flow=$(expr $out_flow_byte / 1024)
  }
 
  FLOW
  in_old=$in_flow
  out_old=$out_flow
 
  sleep 1
 
  FLOW
  in=$in_flow
  out=$out_flow
  dif_in=$(expr $in - $in_old )
  dif_out=$(expr $out - $out_old )
 
  ###Clear the Screen###
  clear
 
  ###Define the safe number###
  servername=Server   #The server's name which you running the scripts
  maxload=5                  #Warning if the load average bigger than this number
  maxcpu=50                 #Warning if the cpu bigger than is percent
  minmem=120             #Warning if the memory less than this number (MB)
  minswap=120            #Warning if the swap less than this number (MB)
  maxusers=3               #Warning if the user online more than 3
  maxzombie=2            #Warning if the zombie process more than 2
 
  ###Check the CPU###
  cpuinfo=$(grep "Cpu(s)" top.info|awk '{print $2}'|cut -d, -f1)
  cpuinfonumber=$(echo $cpuinfo |awk -F "%" '{print $1}'|awk -F "." '{print $1}')
  if [ ${maxcpu} -lt ${cpuinfonumber} ]; then
    echo "!!!!WARNING:The CPU used too much!!!!"
  fi
   
  ###Check the Mem###
  meminfo=$(grep "Mem:" top.info|awk '{print $4 "/" $2}')
  usedmem_kb=$(grep "Mem:" top.info|awk '{print $4}'|awk -F "k" '{print $1}')
  usedmem_mb=$(expr $usedmem_kb / 1024)
  totalmem_kb=$(grep "Mem:" top.info|awk '{print $2}'|awk -F "k" '{print $1}')
  totalmem_mb=$(expr $totalmem_kb / 1024)
  freemem_mb=$(expr $totalmem_mb - $usedmem_mb)
  if [ ${freemem_mb} -lt ${minmem} ]; then
    echo "!!!!WARNING:The Memory used too much!!!!"
  fi
  totalmem_kb_percent=$(expr $totalmem_kb / 100)
  percentmem=$(expr $usedmem_kb / $totalmem_kb_percent)
  
  ###Check the Swap###
  swapinfo=$(grep "Swap:" top.info |awk '{print $4 "/" $2}')
  usedswap_kb=$(grep "Swap:" top.info |awk '{print $4}'|awk -F "k" '{print $1}')
  usedswap_mb=$(expr $usedswap_kb / 1024)
  totalswap_kb=$(grep "Swap:" top.info |awk '{print $2}'|awk -F "k" '{print $1}')
  totalswap_mb=$(expr $totalswap_kb / 1024)
  freeswap_mb=$(expr $totalswap_mb - $usedswap_mb)
  if [ ${freeswap_mb} -lt ${minswap} ]; then
    echo "!!!!WARNING:The Swap used too much!!!!"
  fi
  totalswap_kb_percent=$(expr $totalswap_kb / 100)
  percentswap=$(expr $usedswap_kb / $totalswap_kb_percent)
   
  ###Check the users online###
  useronl=$(grep "load average:" top.info |sed 's/user.*//'|awk '{print $(NF)}')
  if [ ${maxusers} -lt ${useronl} ]; then
    echo "!!!!WARNING:The User online is more than 3!!!!" 
  fi
  
  ###Check the load average###
  loadavg=$(grep "load average:" top.info |sed 's/.*load average: //'|awk -F "," '{print $(NF-2)}')
  loadavgnumber=$(grep "load average:" top.info |sed 's/.*load average: //'|awk -F "," '{print $(NF-2)}'|awk -F "." '{print $1}')
  if [ ${maxload} -lt ${loadavgnumber} ]; then
    echo "!!!!WARNING:The Load Average is more than 5!!!!"
  fi

  ###Check the zombie###
  zombie=$(grep "Tasks:" top.info |awk '{print $(NF-1)}')
  if [ ${maxzombie} -lt ${zombie} ]; then
    echo "!!!!WARNING:The Zombie Process is more than 2!!!!" 
  fi
  
  ###Check the most process###
  processtitle=$(grep "TIME+" top.info)
  processinfo=$(sed -n 8,17p top.info)
 
  ###Show me the number just have counted###
  echo "Server     CPU   LOAD ZOMBIE   USER   IN     OUT             MEM             SWAP    "
  echo "${servername}  ${cpuinfo} ${loadavg}    ${zombie}        ${useronl}   ${dif_in}KB/s ${dif_out}KB/s   ${usedmem_mb}MB/${totalmem_mb}MB ${percentmem}%   ${usedswap_mb}MB/${totalswap_mb}MB ${percentswap}% "
  echo "--------------------------------------------------------------------"
  echo "${processtitle}"
  echo "${processinfo}"
 
done

No Comments

在Linux下用Shell写的俄罗斯方块程序

转载自http://bbs.chinaunix.net/thread-184858-1-1.html

touch tetris.sh
chmod +x tetris.sh
vim tetris.sh

#!/bin/bash
# Tetris Game
# 10.21.2003 xhchen<[email]xhchen@winbond.com.tw[/email]>

#APP declaration
APP_NAME="${0##*[\\/]}"
APP_VERSION="1.0"

#颜色定义
cRed=1
cGreen=2
cYellow=3
cBlue=4
cFuchsia=5
cCyan=6
cWhite=7
colorTable=($cRed $cGreen $cYellow $cBlue $cFuchsia $cCyan $cWhite)

#位置和大小
iLeft=3
iTop=2
((iTrayLeft = iLeft + 2))
((iTrayTop = iTop + 1))
((iTrayWidth = 10))
((iTrayHeight = 15))

#颜色设置
cBorder=$cGreen
cScore=$cFuchsia
cScoreValue=$cCyan

#控制信号
#改游戏使用两个进程,一个用于接收输入,一个用于游戏流程和显示界面;
#当前者接收到上下左右等按键时,通过向后者发送signal的方式通知后者。
sigRotate=25
sigLeft=26
sigRight=27
sigDown=28
sigAllDown=29
sigExit=30

#七中不同的方块的定义
#通过旋转,每种方块的显示的样式可能有几种
box0=(0 0 0 1 1 0 1 1)
box1=(0 2 1 2 2 2 3 2 1 0 1 1 1 2 1 3)
box2=(0 0 0 1 1 1 1 2 0 1 1 0 1 1 2 0)
box3=(0 1 0 2 1 0 1 1 0 0 1 0 1 1 2 1)
box4=(0 1 0 2 1 1 2 1 1 0 1 1 1 2 2 2 0 1 1 1 2 0 2 1 0 0 1 0 1 1 1 2)
box5=(0 1 1 1 2 1 2 2 1 0 1 1 1 2 2 0 0 0 0 1 1 1 2 1 0 2 1 0 1 1 1 2)
box6=(0 1 1 1 1 2 2 1 1 0 1 1 1 2 2 1 0 1 1 0 1 1 2 1 0 1 1 0 1 1 1 2)

#所有其中方块的定义都放到box变量中
box=(${box0[@]} ${box1[@]} ${box2[@]} ${box3[@]} ${box4[@]} ${box5[@]} ${box6[@]})

#各种方块旋转后可能的样式数目
countBox=(1 2 2 2 4 4 4)

#各种方块再box数组中的偏移
offsetBox=(0 1 3 5 7 11 15)

#每提高一个速度级需要积累的分数
iScoreEachLevel=50  #be greater than 7

#运行时数据
sig=0   #接收到的signal
iScore=0  #总分
iLevel=0  #速度级
boxNew=() #新下落的方块的位置定义
cBoxNew=0 #新下落的方块的颜色
iBoxNewType=0 #新下落的方块的种类
iBoxNewRotate=0 #新下落的方块的旋转角度
boxCur=() #当前方块的位置定义
cBoxCur=0 #当前方块的颜色
iBoxCurType=0 #当前方块的种类
iBoxCurRotate=0 #当前方块的旋转角度
boxCurX=-1  #当前方块的x坐标位置
boxCurY=-1  #当前方块的y坐标位置
iMap=()   #背景方块图表

#初始化所有背景方块为-1, 表示没有方块
for ((i = 0; i < iTrayHeight * iTrayWidth; i++)); do iMap[$i]=-1; done

#接收输入的进程的主函数
function RunAsKeyReceiver()
{
  local pidDisplayer key aKey sig cESC sTTY
  pidDisplayer=$1
  aKey=(0 0 0)
  cESC=`echo -ne "\033"`
  cSpace=`echo -ne "\040"`

  #保存终端属性。在read -s读取终端键时,终端的属性会被暂时改变。
  #如果在read -s时程序被不幸杀掉,可能会导致终端混乱,
  #需要在程序退出时恢复终端属性。
  sTTY=`stty -g`

  #捕捉退出信号
  trap "MyExit;" INT TERM
  trap "MyExitNoSub;" $sigExit

  #隐藏光标
  echo -ne "\033[?25l"
  while :
  do
    #读取输入。注-s不回显,-n读到一个字符立即返回
    read -s -n 1 key
    aKey[0]=${aKey[1]}
    aKey[1]=${aKey[2]}
    aKey[2]=$key
    sig=0

    #判断输入了何种键
    if [[ $key == $cESC && ${aKey[1]} == $cESC ]]
    then
      #ESC键
      MyExit
    elif [[ ${aKey[0]} == $cESC && ${aKey[1]} == "[" ]]
    then
      if [[ $key == "A" ]]; then sig=$sigRotate #<向上键>
      elif [[ $key == "B" ]]; then sig=$sigDown #<向下键>
      elif [[ $key == "D" ]]; then sig=$sigLeft #<向左键>
      elif [[ $key == "C" ]]; then sig=$sigRight  #<向右键>
      fi
    elif [[ $key == "W" || $key == "w" ]]; then sig=$sigRotate  #W, w
    elif [[ $key == "S" || $key == "s" ]]; then sig=$sigDown  #S, s
    elif [[ $key == "A" || $key == "a" ]]; then sig=$sigLeft  #A, a
    elif [[ $key == "D" || $key == "d" ]]; then sig=$sigRight #D, d
    elif [[ "[$key]" == "[]" ]]; then sig=$sigAllDown #空格键
    elif [[ $key == "Q" || $key == "q" ]]     #Q, q
    then
      MyExit
    fi
    if [[ $sig != 0 ]]
    then
      #向另一进程发送消息
      kill -$sig $pidDisplayer
    fi
  done
}

#退出前的恢复
function MyExitNoSub()
{
  local y

  #恢复终端属性
  stty $sTTY
  ((y = iTop + iTrayHeight + 4))

  #显示光标
  echo -e "\033[?25h\033[${y};0H"
  exit
}

function MyExit()
{
  #通知显示进程需要退出
  kill -$sigExit $pidDisplayer
  MyExitNoSub
}

#处理显示和游戏流程的主函数
function RunAsDisplayer()
{
  local sigThis
  InitDraw

  #挂载各种信号的处理函数
  trap "sig=$sigRotate;" $sigRotate
  trap "sig=$sigLeft;" $sigLeft
  trap "sig=$sigRight;" $sigRight
  trap "sig=$sigDown;" $sigDown
  trap "sig=$sigAllDown;" $sigAllDown
  trap "ShowExit;" $sigExit
  while :
  do
    #根据当前的速度级iLevel不同,设定相应的循环的次数
    for ((i = 0; i < 21 - iLevel; i++))
    do
      sleep 0.02
      sigThis=$sig
      sig=0

      #根据sig变量判断是否接受到相应的信号
      if ((sigThis == sigRotate)); then BoxRotate;  #旋转
      elif ((sigThis == sigLeft)); then BoxLeft;  #左移一列
      elif ((sigThis == sigRight)); then BoxRight;  #右移一列
      elif ((sigThis == sigDown)); then BoxDown;  #下落一行
      elif ((sigThis == sigAllDown)); then BoxAllDown;  #下落到底
      fi
    done
    #kill -$sigDown $$
    BoxDown #下落一行
  done
}

#BoxMove(y, x), 测试是否可以把移动中的方块移到(x, y)的位置, 返回0则可以, 1不可以
function BoxMove()
{
  local j i x y xTest yTest
  yTest=$1
  xTest=$2
  for ((j = 0; j < 8; j += 2))
  do
    ((i = j + 1))
    ((y = ${boxCur[$j]} + yTest))
    ((x = ${boxCur[$i]} + xTest))
    if (( y < 0 || y >= iTrayHeight || x < 0 || x >= iTrayWidth))
    then
      #撞到墙壁了
      return 1
    fi
    if ((${iMap[y * iTrayWidth + x]} != -1 ))
    then
      #撞到其他已经存在的方块了
      return 1
    fi
  done
  return 0;
}

#将当前移动中的方块放到背景方块中去,
#并计算新的分数和速度级。(即一次方块落到底部)
function Box2Map()
{
  local j i x y xp yp line

  #将当前移动中的方块放到背景方块中去
  for ((j = 0; j < 8; j += 2))
  do
    ((i = j + 1))
    ((y = ${boxCur[$j]} + boxCurY))
    ((x = ${boxCur[$i]} + boxCurX))
    ((i = y * iTrayWidth + x))
    iMap[$i]=$cBoxCur
  done

  #消去可被消去的行
  line=0
  for ((j = 0; j < iTrayWidth * iTrayHeight; j += iTrayWidth))
  do
    for ((i = j + iTrayWidth - 1; i >= j; i--))
    do
      if ((${iMap[$i]} == -1)); then break; fi
    done
    if ((i >= j)); then continue; fi
    ((line++))
    for ((i = j - 1; i >= 0; i--))
    do
      ((x = i + iTrayWidth))
      iMap[$x]=${iMap[$i]}
    done
    for ((i = 0; i < iTrayWidth; i++))
    do
      iMap[$i]=-1
    done
  done
  if ((line == 0)); then return; fi

  #根据消去的行数line计算分数和速度级
  ((x = iLeft + iTrayWidth * 2 + 7))
  ((y = iTop + 11))
  ((iScore += line * 2 - 1))

  #显示新的分数
  echo -ne "\033[1m\033[3${cScoreValue}m\033[${y};${x}H${iScore}         "
  if ((iScore % iScoreEachLevel < line * 2 - 1))
  then
    if ((iLevel < 20))
    then
      ((iLevel++))
      ((y = iTop + 14))

      #显示新的速度级
      echo -ne "\033[3${cScoreValue}m\033[${y};${x}H${iLevel}        "
    fi
  fi
  echo -ne "\033[0m"

  #重新显示背景方块
  for ((y = 0; y < iTrayHeight; y++))
  do
    ((yp = y + iTrayTop + 1))
    ((xp = iTrayLeft + 1))
    ((i = y * iTrayWidth))
    echo -ne "\033[${yp};${xp}H"
    for ((x = 0; x < iTrayWidth; x++))
    do
      ((j = i + x))
      if ((${iMap[$j]} == -1))
      then
        echo -ne "  "
      else
        echo -ne "\033[1m\033[7m\033[3${iMap[$j]}m\033[4${iMap[$j]}m[]\033[0m"
      fi
    done
  done
}

#下落一行
function BoxDown()
{
  local y s
  ((y = boxCurY + 1)) #新的y坐标
  if BoxMove $y $boxCurX  #测试是否可以下落一行
  then
    s="`DrawCurBox 0`"  #将旧的方块抹去
    ((boxCurY = y))
    s="$s`DrawCurBox 1`"  #显示新的下落后方块
    echo -ne $s
  else
    #走到这儿, 如果不能下落了
    Box2Map   #将当前移动中的方块贴到背景方块中
    RandomBox #产生新的方块
  fi
}

#左移一列
function BoxLeft()
{
  local x s
  ((x = boxCurX - 1))
  if BoxMove $boxCurY $x
  then
    s=`DrawCurBox 0`
    ((boxCurX = x))
    s=$s`DrawCurBox 1`
    echo -ne $s
  fi
}

#右移一列
function BoxRight()
{
  local x s
  ((x = boxCurX + 1))
  if BoxMove $boxCurY $x
  then
    s=`DrawCurBox 0`
    ((boxCurX = x))
    s=$s`DrawCurBox 1`
    echo -ne $s
  fi
}

#下落到底
function BoxAllDown()
{
  local k j i x y iDown s
  iDown=$iTrayHeight
  #计算一共需要下落多少行
  for ((j = 0; j < 8; j += 2))
  do
    ((i = j + 1))
    ((y = ${boxCur[$j]} + boxCurY))
    ((x = ${boxCur[$i]} + boxCurX))
    for ((k = y + 1; k < iTrayHeight; k++))
    do
      ((i = k * iTrayWidth + x))
      if (( ${iMap[$i]} != -1)); then break; fi
    done
    ((k -= y + 1))
    if (( $iDown > $k )); then iDown=$k; fi
  done
  s=`DrawCurBox 0`  #将旧的方块抹去
  ((boxCurY += iDown))
  s=$s`DrawCurBox 1`  #显示新的下落后的方块
  echo -ne $s
  Box2Map   #将当前移动中的方块贴到背景方块中
  RandomBox #产生新的方块
}

#旋转方块
function BoxRotate()
{
  local iCount iTestRotate boxTest j i s
  iCount=${countBox[$iBoxCurType]}  #当前的方块经旋转可以产生的样式的数目

  #计算旋转后的新的样式
  ((iTestRotate = iBoxCurRotate + 1))
  if ((iTestRotate >= iCount))
  then
    ((iTestRotate = 0))
  fi

  #更新到新的样式, 保存老的样式(但不显示)
  for ((j = 0, i = (${offsetBox[$iBoxCurType]} + $iTestRotate) * 8; j < 8; j++, i++))
  do
    boxTest[$j]=${boxCur[$j]}
    boxCur[$j]=${box[$i]}
  done
  if BoxMove $boxCurY $boxCurX  #测试旋转后是否有空间放的下
  then
    #抹去旧的方块
    for ((j = 0; j < 8; j++))
    do
      boxCur[$j]=${boxTest[$j]}
    done
    s=`DrawCurBox 0`

    #画上新的方块
    for ((j = 0, i = (${offsetBox[$iBoxCurType]} + $iTestRotate) * 8; j < 8; j++, i++))
    do
      boxCur[$j]=${box[$i]}
    done
    s=$s`DrawCurBox 1`
    echo -ne $s
    iBoxCurRotate=$iTestRotate
  else
    #不能旋转,还是继续使用老的样式
    for ((j = 0; j < 8; j++))
    do
      boxCur[$j]=${boxTest[$j]}
    done
  fi
}

#DrawCurBox(bDraw), 绘制当前移动中的方块, bDraw为1, 画上, bDraw为0, 抹去方块。
function DrawCurBox()
{
  local i j t bDraw sBox s
  bDraw=$1
  s=""
  if (( bDraw == 0 ))
  then
    sBox="\040\040"
  else
    sBox="[]"
    s=$s"\033[1m\033[7m\033[3${cBoxCur}m\033[4${cBoxCur}m"
  fi
  for ((j = 0; j < 8; j += 2))
  do
    ((i = iTrayTop + 1 + ${boxCur[$j]} + boxCurY))
    ((t = iTrayLeft + 1 + 2 * (boxCurX + ${boxCur[$j + 1]})))

    #\033[y;xH, 光标到(x, y)处
    s=$s"\033[${i};${t}H${sBox}"
  done
  s=$s"\033[0m"
  echo -n $s
}

#更新新的方块
function RandomBox()
{
  local i j t

  #更新当前移动的方块
  iBoxCurType=${iBoxNewType}
  iBoxCurRotate=${iBoxNewRotate}
  cBoxCur=${cBoxNew}
  for ((j = 0; j < ${#boxNew[@]}; j++))
  do
    boxCur[$j]=${boxNew[$j]}
  done

  #显示当前移动的方块
  if (( ${#boxCur[@]} == 8 ))
  then
    #计算当前方块该从顶端哪一行"冒"出来
    for ((j = 0, t = 4; j < 8; j += 2))
    do
      if ((${boxCur[$j]} < t)); then t=${boxCur[$j]}; fi
    done
    ((boxCurY = -t))
    for ((j = 1, i = -4, t = 20; j < 8; j += 2))
    do
      if ((${boxCur[$j]} > i)); then i=${boxCur[$j]}; fi
      if ((${boxCur[$j]} < t)); then t=${boxCur[$j]}; fi
    done
    ((boxCurX = (iTrayWidth - 1 - i - t) / 2))

    #显示当前移动的方块
    echo -ne `DrawCurBox 1`

    #如果方块一出来就没处放,Game over!
    if ! BoxMove $boxCurY $boxCurX
    then
      kill -$sigExit ${PPID}
      ShowExit
    fi
  fi

  #清除右边预显示的方块
  for ((j = 0; j < 4; j++))
  do
    ((i = iTop + 1 + j))
    ((t = iLeft + 2 * iTrayWidth + 7))
    echo -ne "\033[${i};${t}H        "
  done

  #随机产生新的方块
  ((iBoxNewType = RANDOM % ${#offsetBox[@]}))
  ((iBoxNewRotate = RANDOM % ${countBox[$iBoxNewType]}))
  for ((j = 0, i = (${offsetBox[$iBoxNewType]} + $iBoxNewRotate) * 8; j < 8; j++, i++))
  do
    boxNew[$j]=${box[$i]};
  done
  ((cBoxNew = ${colorTable[RANDOM % ${#colorTable[@]}]}))

  #显示右边预显示的方块
  echo -ne "\033[1m\033[7m\033[3${cBoxNew}m\033[4${cBoxNew}m"
  for ((j = 0; j < 8; j += 2))
  do
    ((i = iTop + 1 + ${boxNew[$j]}))
    ((t = iLeft + 2 * iTrayWidth + 7 + 2 * ${boxNew[$j + 1]}))
    echo -ne "\033[${i};${t}H[]"
  done
  echo -ne "\033[0m"
}

#初始绘制
function InitDraw()
{
  clear
  RandomBox #随机产生方块,这时右边预显示窗口中有方快了
  RandomBox #再随机产生方块,右边预显示窗口中的方块被更新,原先的方块将开始下落
  local i t1 t2 t3

  #显示边框
  echo -ne "\033[1m"
  echo -ne "\033[3${cBorder}m\033[4${cBorder}m"
  ((t2 = iLeft + 1))
  ((t3 = iLeft + iTrayWidth * 2 + 3))
  for ((i = 0; i < iTrayHeight; i++))
  do
    ((t1 = i + iTop + 2))
    echo -ne "\033[${t1};${t2}H||"
    echo -ne "\033[${t1};${t3}H||"
  done
  ((t2 = iTop + iTrayHeight + 2))
  for ((i = 0; i < iTrayWidth + 2; i++))
  do
    ((t1 = i * 2 + iLeft + 1))
    echo -ne "\033[${iTrayTop};${t1}H=="
    echo -ne "\033[${t2};${t1}H=="
  done
  echo -ne "\033[0m"

  #显示"Score"和"Level"字样
  echo -ne "\033[1m"
  ((t1 = iLeft + iTrayWidth * 2 + 7))
  ((t2 = iTop + 10))
  echo -ne "\033[3${cScore}m\033[${t2};${t1}HScore"
  ((t2 = iTop + 11))
  echo -ne "\033[3${cScoreValue}m\033[${t2};${t1}H${iScore}"
  ((t2 = iTop + 13))
  echo -ne "\033[3${cScore}m\033[${t2};${t1}HLevel"
  ((t2 = iTop + 14))
  echo -ne "\033[3${cScoreValue}m\033[${t2};${t1}H${iLevel}"
  echo -ne "\033[0m"
}

#退出时显示GameOVer!
function ShowExit()
{
  local y
  ((y = iTrayHeight + iTrayTop + 3))
  echo -e "\033[${y};0HGameOver!\033[0m"
  exit
}

#显示用法.
function Usage
{
  cat << EOF
Usage: $APP_NAME
Start tetris game.
  -h, --help              display this help and exit
      --version           output version information and exit
EOF
}

#游戏主程序在这儿开始.
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
  Usage
elif [[ "$1" == "--version" ]]; then
  echo "$APP_NAME $APP_VERSION"
elif [[ "$1" == "--show" ]]; then
  #当发现具有参数--show时,运行显示函数
  RunAsDisplayer
else
  bash $0 --show& #以参数--show将本程序再运行一遍
  RunAsKeyReceiver $! #以上一行产生的进程的进程号作为参数
fi

./tetris.sh
tetris_shell

No Comments