SHELL脚本编程基础知识

时间:2022-09-08 10:08:02

         SHELL脚本编程基础知识

                       作者:尹正杰

版权声明:原创作品,谢绝转载!否则将追究法律责任。

  Linux之父Linus有一句话很经典:"Talk is cheap, show me the code",虽然我们是一枚小小的运维工程师,但工作中确实是有一些任务是需要写成脚本方式来实现的。在招聘面试过程中,要求运维人员会shell编程是必须的,甚至有的公司得要求运维会Java,Python或者Golang。

一.编程基础概念
1>.程序相关概念
程序:
  算法+数据结构
数据:
  是程序的核心
数据结构:
  数据在计算机中的类型和组织方式
算法:
  处理数据的方式
2>.程序编程风格
过程式:
  以指令为中心,数据服务于指令
对象式:
  以数据为中心,指令服务于数据
3>.shell程序
  shell是一个命令解释器,它在操作系统的最外层,负责直接与用户对话,把用户的输入解释给操作系统,并处理各种各样的操作系统的输出结果,输出到屏幕返回给用户。
 
  这种对话方式可以是交互的方式(从键盘输入命令,可以立即得到shell的回应),或非交互(脚本)的方式。换句话说,Shell是一个命令行解释器,它为用户提供一个像Linux内核发送请求以便运行程序的界面系统级程序,用户可以用Shell来启动,挂起,停止甚至是编写一些程序。
 
  Shell还是一个功能相当强的编程语言,易编写,易调试,灵活性较强。Shell是解释执行的脚本语言,在Shell中可以直接调用Linux系统命令。
SHELL脚本编程基础知识
 
二.程序的执行方式
1>.计算机
  计算机只能识别二进制,因此运行程序其实就是运行二进制指令。
2>.编程语言
  编程语言是人与计算机之间交互的语言。
3>.低级编程语言
机器语言:
  二进制的0和1的序列,称为机器指令。与自然语言差异太大,难懂、难写。
汇编:
  用一些助记符号替代机器指令,称为汇编语言。
    如:ADD A,B 将寄存器A的数与寄存器B的数相加得到的数放到寄存器A中
      汇编语言写好的程序需要汇编程序转换成机器指令
      汇编语言稍微好理解,即机器指令对应的助记符,助记符更接近自然语言
4>.高级编程语言
编译型语言执行过程:高级语言-->编译器-->机器代码-->执行
  典型代表:C,C++,等
  特点:开发效率低,执行效率高 解释型语言执行过程:高级语言-->执行-->解释器-->机器代码
  典型代表:shell,python,php,JavaScript,perl,Scala等
  特点:开发效率高,执行效率低
5>.编译和解释型语言
无论是编译型语言还是解释型语言在外面生活中都能找到类似吃菜的场景:
  炒菜:
    上菜后就直接可用吃啦,但是我们得等炒菜的过程,这就很像我们计算机编译型语言的执行过程。   火锅:
    不需要提前把菜弄熟,而是想吃哪个就先煮哪个菜,边吃火锅里煮着,这就很像我们计算机解释性语言的执行过程。
SHELL脚本编程基础知识

6>.编程逻辑处理方式

顺序执行
循环执行
选择执行
7>.shell编程(过程式、解释执行)
编程语言的基本结构:
  各种系统命令的组合
  数据存储:变量、数组
  表达式:如:"a + b"
  语句:if...else,case,while do...done等
 
三.脚本语言的种类 
1>.php语言
  它是网页程序,也是脚本语言,更专注于web页面的开发,例如:dedecms,discuz。也可以处理系统日志,配置文件等。
2>.perl语言
  perl脚本语言,比shell强大的多,2010年前很火,语法灵活,复杂,实现方法很多,不易读,团队协作困难。
3>.Python语言
  近几年很火的语言,可以做脚本开发,也可以实现web开发(但并不是和做电商网站开发哟~),中等以上的公司都要求会python。
4>.shell语言
  最容易上手的脚本,shell的优势在于处理操作系统底层的业务,因为有大量的系统命令为它做支撑,2000多个命令都是shell编程的有力支撑,特别是grep,awk,sed等。例如:一件软件安装,优化,监控报警脚本,常规的业务应用,shell开发更简单快速。以下是解释器支持的shell类型。
Golang语言,一个很适合做自动化运维的编程语言
它是编译型语言,Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程,很适合做系统开发或自动化运维。

Golang语言,一个很适合做自动化运维的编程语言

四.Linux支持的Shell
Bourne Shell:
  从1979年起Unix就开始使用Bourne Shell,Bourne Shell的主文件名为sh,我们最常见的"/bin/sh"。 C Shell:
  C Shell主要在BSD版的Unix系统中使用,其语法和C语言相类似而得名,我们最常见的就是“/bin/csh”。 Bash:
  Bash与sh兼容,现在使用的Linux就是使用Bash作为用户的基本Shell。
  
温馨提示:
  Shell的两种主要语法类型有Bourne和C,这两种语法彼此不兼容。
  Bourne家族主要包括sh,ksh,Bash,psh,zsh;
  C家族主要包括:csh,tcsh。 常用操作系统的默认Shell
  linux操作系统:
    Bourne Again shell(bash)。
  Solaris和FreeBSD操作系统:
    Bourne shell(sh)。
  AIX操作系统:
    Korn Shell(ksh)。
  HP-UX操作系统:
    POSIX shell(sh)。
  Centos linux操作系统:
    默认的shell是bash。
[root@node101.yinzhengjie.org.cn ~]# cat /etc/shells     #这个文件保存着当前操作系统支持的Shell版本
/bin/sh
/bin/bash
/usr/bin/sh
/usr/bin/bash
[root@node101.yinzhengjie.org.cn ~]#

[root@node101.yinzhengjie.org.cn ~]# cat /etc/shells     #该文件保存着当前操作系统支持的Shell版本

五.shell脚本基础
1>.什么是shelll脚本
  包含一些命令或声明,并符合一定格式的文本文件
2>.格式要求
编写脚本的时候我们通常会在第一行指定当前脚本所用的解释权,我们称之为"首行shebang机制",这里的"shebang"其实就是"#!"的读音翻译。

我们写脚本的时候一般都是调用bash,所以第一行我们要写上这么一行:"#!/bin/bash",要注意的是这可不是注释行哟~而是告诉内核我们用的是哪种解释器,下面的所有行,如果在出现类似的内核都会认为是注释行。

常见解释性语言的开头标识内容如下:
  #!/bin/bash
  #!/bin/sh
  #!/usr/bin/awk
  #!/bin/sed
  #!/usr/bin/tcl
  #!/usr/bin/expect
  #!/usr/bin/perl
  #!/usr/bin/env python
3>.shell脚本的用途
自动化常用命令

执行系统管理和故障排除

创建简单的应用程序

处理文本或文件
六.创建第一个shell脚本
1>.使用文本编辑器来创建文本文件
[root@node101.yinzhengjie.org.cn ~]# mkdir -pv /data/script
mkdir: created directory ‘/data’
mkdir: created directory ‘/data/script’
[root@node101.yinzhengjie.org.cn ~]#
[root@node101.yinzhengjie.org.cn ~]# cd /data/script/
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# vim hello.sh
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# cat hello.sh
#!/bin/bash
#@author:yinzhengjie
#blog:http://www.cnblogs.com/yinzhengjie
#Description: This is the first script echo "hello world"
echo "My hostname is `hostname`"
[root@node101.yinzhengjie.org.cn /data/script]#
2>.运行脚本
[root@node101.yinzhengjie.org.cn /data/script]# ll
total
-rw-r--r-- root root Nov : hello.sh
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# chmod +x hello.sh #给予执行权限,在命令行上指定脚本的绝对或相对路径
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# ll
total
-rwxr-xr-x root root Nov : hello.sh
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# ./hello.sh     #有执行权限后,可用用相对路径调用执行
hello world
My hostname is node101.yinzhengjie.org.cn
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# /data/script/hello.sh   #有执行权限后,也可以直接使用绝对路径调用执行
hello world
My hostname is node101.yinzhengjie.org.cn
[root@node101.yinzhengjie.org.cn /data/script]#

给予执行权限,在命令行上指定脚本的绝对或相对路径

[root@node101.yinzhengjie.org.cn /data/script]# ll
total
-rwxr-xr-x root root Nov : hello.sh
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# chmod -x hello.sh
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# ll
total
-rw-r--r-- root root Nov : hello.sh
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# sh hello.sh #直接运行解释器,将脚本作为解释器程序的参数运行
hello world
My hostname is node101.yinzhengjie.org.cn
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]#

直接运行解释器,将脚本作为解释器程序的参数运行

[root@node101.yinzhengjie.org.cn /data/script]# echo $PATH
/usr/lib64/qt-3.3/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/home/softwares/jdk1..0_201/bin:/root/bin:/home/softwares/mysql/bin/
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# vim /etc/profile.d/env.sh
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# cat /etc/profile.d/env.sh
PATH=/data/script:$PATH
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# source /etc/profile.d/env.sh
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# echo $PATH
/data/script:/usr/lib64/qt-3.3/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/home/softwares/jdk1..0_201/bin:/root/bin:/home/softwares/mysql/bin/
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# he
head help hexdump
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# ll
total
-rw-r--r-- root root Nov : hello.sh
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# chmod +x hello.sh
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# ll
total
-rwxr-xr-x root root Nov : hello.sh
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# cd
[root@node101.yinzhengjie.org.cn ~]#
[root@node101.yinzhengjie.org.cn ~]# hello.sh
hello world
My hostname is node101.yinzhengjie.org.cn
[root@node101.yinzhengjie.org.cn ~]#
[root@node101.yinzhengjie.org.cn ~]#

将编写脚本路径添加到"$PATH"环境变量中,只要有执行权限的脚本文件均可被调用执行

3>. "source"和"."还有"sh"调用脚本的区别
  "source"和"."的功能是一样的,可以调用脚本,并将脚本里的函数也传递到当前的脚本或者解释器中,即不会开启新的bash而是在当前bash中运行。

  "sh"后面跟脚本名称,则不会将该脚本的函数传递进来,即需要开启新的bash,"sh"实际上是执行一个脚本,最后执行完毕会将内存释放掉,不会保存变量。

  而"."和"source"则不会新的bash进程,这就是为什么在/etc/init.d/这个目录下有很多的脚本都会用"."去调用脚本。

  综上所述:
    生产环境中编写脚本一般会使用sh命令去执行脚本,因为使用sh命令执行的脚本执行完毕后会自动释放内存并不会影响当前进程中的变量。除非你明确直到想要脚本中的变量要在当前bash中生效(比如重新读取配置文件)则可以使用"source"或者"."
[root@node101.yinzhengjie.org.cn ~]# vim shell/test.sh
[root@node101.yinzhengjie.org.cn ~]#
[root@node101.yinzhengjie.org.cn ~]# cat shell/test.sh
#!/bin/bash
#
#********************************************************************
#Author: yinzhengjie
#QQ:
#Date: --
#FileName: test.sh
#URL: http://www.cnblogs.com/yinzhengjie
#Description: The test script
#Copyright notice: original works, no reprint! Otherwise, legal liability will be investigated.
#******************************************************************** NAME="尹正杰" #定义一个变量
echo $NAME #打印咱们的上面定义的变量对应的值
echo $BASHPID #打印当前脚本执行的bash的pid编号
sleep #让脚本晚一点结束,咱们可以使用pstree命令观察是否有新的进程生成。
[root@node101.yinzhengjie.org.cn ~]#
[root@node101.yinzhengjie.org.cn ~]# NAME="Jason Yin"
[root@node101.yinzhengjie.org.cn ~]# echo $NAME         #这是打印的在当前shell中定义的变量
Jason Yin
[root@node101.yinzhengjie.org.cn ~]#
[root@node101.yinzhengjie.org.cn ~]# bash shell/test.sh     #不难发现我们在脚本中定义了变量被打印出来了,
尹正杰 [root@node101.yinzhengjie.org.cn ~]# echo $NAME          #我们发现执行完脚本后当前shell的变量并没有被覆盖哟~
Jason Yin
[root@node101.yinzhengjie.org.cn ~]# 上面代码进入阻塞状态时,咱们开启新的终端执行如下命令:
[root@node101.yinzhengjie.org.cn ~]# pstree -p | grep sshd    #可以看到有一个sleep进程id为14814,而其父进程则为14813哟,结合上面的输出进行对比。
|-sshd()-+-sshd()---bash()-+-grep()
| `-sshd()---bash()---bash()---sleep()
[root@node101.yinzhengjie.org.cn ~]#

sh案例(在CentOS操作系统sh其实就是bash命令的软连接)

[root@node101.yinzhengjie.org.cn ~]# vim shell/test.sh
[root@node101.yinzhengjie.org.cn ~]#
[root@node101.yinzhengjie.org.cn ~]# cat shell/test.sh
#!/bin/bash
#
#********************************************************************
#Author: yinzhengjie
#QQ:
#Date: --
#FileName: test.sh
#URL: http://www.cnblogs.com/yinzhengjie
#Description: The test script
#Copyright notice: original works, no reprint! Otherwise, legal liability will be investigated.
#******************************************************************** NAME="尹正杰" #定义一个变量
echo $NAME #打印咱们的上面定义的变量对应的值
echo $BASHPID #打印当前脚本执行的bash的pid编号
sleep #让脚本晚一点结束,咱们可以使用pstree命令观察是否有新的进程生成。
[root@node101.yinzhengjie.org.cn ~]#
[root@node101.yinzhengjie.org.cn ~]# NAME="Jason Yin"
[root@node101.yinzhengjie.org.cn ~]# echo $NAME
Jason Yin
[root@node101.yinzhengjie.org.cn ~]# [root@node101.yinzhengjie.org.cn ~]# chmod +x shell/test.sh
[root@node101.yinzhengjie.org.cn ~]#
[root@node101.yinzhengjie.org.cn ~]# ./shell/test.sh
尹正杰 当执行上面的脚本进入阻塞状态时,在其它终端执行以下命令(发现有执行权限的脚本在执行代码时也开启了新的bash):
[root@node101.yinzhengjie.org.cn ~]# pstree -p | grep sshd
|-sshd()-+-sshd()---bash()-+-grep()
| `-sshd()---bash()---test.sh()---sleep()
[root@node101.yinzhengjie.org.cn ~]#

发现有执行权限的脚本在执行代码时也开启了新的bash

 
七.脚本规范

1>.脚本开头约定

开头执行脚本解释器

开头加版权等信息

脚本中尽量不要用中文注释,尽量用英文注释,防止本机或切换系统环境后中文乱码的困扰
脚本以".sh"为扩展名

代码书写优秀习惯
().成对内容的一次写出来,防止漏写。例如:{},[],'',``,"".
().[]中括号两端都要有空格,书写时即可流出空格[ ],然后退格书写内容。
().流程控制语句一次书写完,在添加内容。

2>.脚本的基本结构

#!SHEBANG
CONFIGURATION_VARIABLES
FUNCTION_DEFINITIONS
MAIN_CODE

3>.shell脚本示例

[root@node101.yinzhengjie.org.cn /data/script]# cat hello.sh
#!/bin/bash
#
#********************************************************************
#Author: yinzhengjie
#Email: y1053419035@qq.com
#Date: --
#FileName: hello.sh
#URL: http://www.cnblogs.com/yinzhengjie
#Description: This is the first script
#Copyright (C): Original works, no reprint! Otherwise, legal liability will be investigated
#******************************************************************** echo -e "hello world"
echo -e "My hostname is `hostname`"
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]#

4>.可配置vim编辑文件时自动加上以上信息版权信息(方法是修改~/.vimrc配置文件)

[root@node101.yinzhengjie.org.cn ~]# cat ~/.vimrc         #下面附有对每行的中文解释,使用时需要将这些中文字符提前删除哟~
set ignorecase #忽略大小写
set cursorline #移动光标时添加下划线
set autoindent #自动进行行对其
autocmd BufNewFile *.sh exec ":call SetTitle()" #当打开是以"*.sh"的文件名称时,就会自动调用下面我们定义的函数啦! func SetTitle()
if expand("%:e") == 'sh'
call setline(,"#!/bin/bash")
call setline(,"#")
call setline(,"#********************************************************************")
call setline(,"#Author: yinzhengjie")
call setline(,"#QQ: 1053419035")
call setline(,"#Date: ".strftime("%Y-%m-%d"))
call setline(,"#FileName: ".expand("%"))
call setline(,"#URL: http://www.cnblogs.com/yinzhengjie")
call setline(,"#Description: The test script")
call setline(,"#Copyright (C): ".strftime("%Y")." All rights reserved")
call setline(,"#********************************************************************")
call setline(,"")
endif
endfunc
autocmd BufNewFile * normal G
[root@node101.yinzhengjie.org.cn ~]#
 
八.脚本调试
1>.检测脚本中的语法错误
[root@node101.yinzhengjie.org.cn /data/script]# ll
total
-rwxr-xr-x root root Nov : hello.sh
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# bash -n hello.sh     #检查脚本的语法并不会执行,若有语法错误就会抛出异常,只能检查语法错误不能检查命令是否错误。
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]# bash hello.sh       #调用脚本并执行
hello world
My hostname is node101.yinzhengjie.org.cn
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]#

[root@node101.yinzhengjie.org.cn /data/script]# bash -n hello.sh   #检查脚本的语法并不会执行

2>.调试执行
[root@node101.yinzhengjie.org.cn /data/script]# bash -x  hello.sh     #查看脚本的执行过程,尤其时代表出错时,可用看到是具体哪行代码出错啦~
+ echo 'hello world'
hello world
++ hostname
+ echo 'My hostname is node101.yinzhengjie.org.cn'
My hostname is node101.yinzhengjie.org.cn
[root@node101.yinzhengjie.org.cn /data/script]#
[root@node101.yinzhengjie.org.cn /data/script]#

[root@node101.yinzhengjie.org.cn /data/script]# bash -x hello.sh     #查看脚本的执行过程

九.bash漏洞修复方式
1>.背景
  网络安全专家警告称,开源软件Linux中一个频繁使用的片段“Bash”,发现存在安全漏洞,其对计算机用户造成的威胁可能要超过2014年4月爆出的"心脏出血"(Heartbleed)漏洞。
2>.漏洞原理
  bash是用于控制Linux计算机命令提示符的软件。网络安全专家表示,黑客可以利用Bash中的一个安全漏洞,对目标计算机系统进行完全控制。

  网络安全公司Trail of Bits的首席执行官丹·吉多(Dan Guido)指出:与"Heartbleed"相比,后者只允许黑客窥探计算机,但不会让黑客获得计算机的控制权。"他说:"利用Bash漏洞的方法也简单得多,你可以直接剪切和粘贴一行软件代码,就能取得很好的效果。"吉多还表示,他正考虑将自己公司非必要的服务器断网,以保护他们不会受到Bash漏洞的攻击,直到他能够修补这一漏洞为止。

  网络安全公司Rapid7的工程经理托德·比尔兹利(Tod Beardsley)则警告称,Bash漏洞的严重程度被评为10级,意味着它具有最大的影响力,而其利用的难度被评为"低"级,意味着黑客比较容易地利用其发动网络攻击。

  比尔兹利称:"利用这个漏洞,攻击者可能会接管计算机的整个操作系统,得以访问机密信息,并对系统进行更改等等。任何人的计算机系统,如果使用了Bash软件,都需要立即打上补丁。"
3>.修复方式
centos系统(如果是centos系统只要运行下面简单的命令就可以)
  yum clean all
  yum makecache
  yum -y update bash Ubuntu系统
  apt-get update
  apt-get -y install --only-upgrade bash Debian系统
  如果是7. 64位 && 32位环境运行
  apt-get update
  apt-get -y install --only-upgrade bash
十.扩展小知识:Shell编写的俄罗斯方块。
#!/bin/bash

#APP declaration
APP_NAME="${0##*[\\/]}"
APP_VERSION="1.0" #颜色定义
cRed=
cGreen=
cYellow=
cBlue=
cFuchsia=
cCyan=
cWhite=
colorTable=($cRed $cGreen $cYellow $cBlue $cFuchsia $cCyan $cWhite) #位置和大小
iLeft=
iTop=
((iTrayLeft = iLeft + ))
((iTrayTop = iTop + ))
((iTrayWidth = ))
((iTrayHeight = )) #颜色设置
cBorder=$cGreen
cScore=$cFuchsia
cScoreValue=$cCyan #控制信号
#改游戏使用两个进程,一个用于接收输入,一个用于游戏流程和显示界面;
#当前者接收到上下左右等按键时,通过向后者发送signal的方式通知后者。
sigRotate=
sigLeft=
sigRight=
sigDown=
sigAllDown=
sigExit= #七中不同的方块的定义
#通过旋转,每种方块的显示的样式可能有几种
box0=( )
box1=( )
box2=( )
box3=( )
box4=( )
box5=( )
box6=( )
#所有其中方块的定义都放到box变量中
box=(${box0[@]} ${box1[@]} ${box2[@]} ${box3[@]} ${box4[@]} ${box5[@]} ${box6[@]})
#各种方块旋转后可能的样式数目
countBox=( )
#各种方块再box数组中的偏移
offsetBox=( ) #每提高一个速度级需要积累的分数
iScoreEachLevel= #be greater than #运行时数据
sig= #接收到的signal
iScore= #总分
iLevel= #速度级
boxNew=() #新下落的方块的位置定义
cBoxNew= #新下落的方块的颜色
iBoxNewType= #新下落的方块的种类
iBoxNewRotate= #新下落的方块的旋转角度
boxCur=() #当前方块的位置定义
cBoxCur= #当前方块的颜色
iBoxCurType= #当前方块的种类
iBoxCurRotate= #当前方块的旋转角度
boxCurX=- #当前方块的x坐标位置
boxCurY=- #当前方块的y坐标位置
iMap=() #背景方块图表 #初始化所有背景方块为-, 表示没有方块
for ((i = ; i < iTrayHeight * iTrayWidth; i++)); do iMap[$i]=-; done #接收输入的进程的主函数
function RunAsKeyReceiver()
{
local pidDisplayer key aKey sig cESC sTTY pidDisplayer=$
aKey=( ) 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 key aKey[]=${aKey[]}
aKey[]=${aKey[]}
aKey[]=$key
sig= #判断输入了何种键
if [[ $key == $cESC && ${aKey[]} == $cESC ]]
then
#ESC键
MyExit
elif [[ ${aKey[]} == $cESC && ${aKey[]} == "[" ]]
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 != ]]
then
#向另一进程发送消息
kill -$sig $pidDisplayer
fi
done
} #退出前的恢复
function MyExitNoSub()
{
local y #恢复终端属性
stty $sTTY
((y = iTop + iTrayHeight + )) #显示光标
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 = ; i < - iLevel; i++))
do
sleep 0.02
sigThis=$sig
sig= #根据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=$
xTest=$
for ((j = ; j < ; j += ))
do
((i = j + ))
((y = ${boxCur[$j]} + yTest))
((x = ${boxCur[$i]} + xTest))
if (( y < || y >= iTrayHeight || x < || x >= iTrayWidth))
then
#撞到墙壁了
return
fi
if ((${iMap[y * iTrayWidth + x]} != - ))
then
#撞到其他已经存在的方块了
return
fi
done
return ;
} #将当前移动中的方块放到背景方块中去,
#并计算新的分数和速度级。(即一次方块落到底部)
function Box2Map()
{
local j i x y xp yp line #将当前移动中的方块放到背景方块中去
for ((j = ; j < ; j += ))
do
((i = j + ))
((y = ${boxCur[$j]} + boxCurY))
((x = ${boxCur[$i]} + boxCurX))
((i = y * iTrayWidth + x))
iMap[$i]=$cBoxCur
done #消去可被消去的行
line=
for ((j = ; j < iTrayWidth * iTrayHeight; j += iTrayWidth))
do
for ((i = j + iTrayWidth - ; i >= j; i--))
do
if ((${iMap[$i]} == -)); then break; fi
done
if ((i >= j)); then continue; fi ((line++))
for ((i = j - ; i >= ; i--))
do
((x = i + iTrayWidth))
iMap[$x]=${iMap[$i]}
done
for ((i = ; i < iTrayWidth; i++))
do
iMap[$i]=-
done
done if ((line == )); then return; fi #根据消去的行数line计算分数和速度级
((x = iLeft + iTrayWidth * + ))
((y = iTop + ))
((iScore += line * - ))
#显示新的分数
echo -ne "\033[1m\033[3${cScoreValue}m\033[${y};${x}H${iScore} "
if ((iScore % iScoreEachLevel < line * - ))
then
if ((iLevel < ))
then
((iLevel++))
((y = iTop + ))
#显示新的速度级
echo -ne "\033[3${cScoreValue}m\033[${y};${x}H${iLevel} "
fi
fi
echo -ne "\033[0m" #重新显示背景方块
for ((y = ; y < iTrayHeight; y++))
do
((yp = y + iTrayTop + ))
((xp = iTrayLeft + ))
((i = y * iTrayWidth))
echo -ne "\033[${yp};${xp}H"
for ((x = ; x < iTrayWidth; x++))
do
((j = i + x))
if ((${iMap[$j]} == -))
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 + )) #新的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 - ))
if BoxMove $boxCurY $x
then
s=`DrawCurBox `
((boxCurX = x))
s=$s`DrawCurBox `
echo -ne $s
fi
} #右移一列
function BoxRight()
{
local x s
((x = boxCurX + ))
if BoxMove $boxCurY $x
then
s=`DrawCurBox `
((boxCurX = x))
s=$s`DrawCurBox `
echo -ne $s
fi
} #下落到底
function BoxAllDown()
{
local k j i x y iDown s
iDown=$iTrayHeight #计算一共需要下落多少行
for ((j = ; j < ; j += ))
do
((i = j + ))
((y = ${boxCur[$j]} + boxCurY))
((x = ${boxCur[$i]} + boxCurX))
for ((k = y + ; k < iTrayHeight; k++))
do
((i = k * iTrayWidth + x))
if (( ${iMap[$i]} != -)); then break; fi
done
((k -= y + ))
if (( $iDown > $k )); then iDown=$k; fi
done s=`DrawCurBox ` #将旧的方块抹去
((boxCurY += iDown))
s=$s`DrawCurBox ` #显示新的下落后的方块
echo -ne $s
Box2Map #将当前移动中的方块贴到背景方块中
RandomBox #产生新的方块
} #旋转方块
function BoxRotate()
{
local iCount iTestRotate boxTest j i s
iCount=${countBox[$iBoxCurType]} #当前的方块经旋转可以产生的样式的数目 #计算旋转后的新的样式
((iTestRotate = iBoxCurRotate + ))
if ((iTestRotate >= iCount))
then
((iTestRotate = ))
fi #更新到新的样式, 保存老的样式(但不显示)
for ((j = , i = (${offsetBox[$iBoxCurType]} + $iTestRotate) * ; j < ; j++, i++))
do
boxTest[$j]=${boxCur[$j]}
boxCur[$j]=${box[$i]}
done if BoxMove $boxCurY $boxCurX #测试旋转后是否有空间放的下
then
#抹去旧的方块
for ((j = ; j < ; j++))
do
boxCur[$j]=${boxTest[$j]}
done
s=`DrawCurBox ` #画上新的方块
for ((j = , i = (${offsetBox[$iBoxCurType]} + $iTestRotate) * ; j < ; j++, i++))
do
boxCur[$j]=${box[$i]}
done
s=$s`DrawCurBox `
echo -ne $s
iBoxCurRotate=$iTestRotate
else
#不能旋转,还是继续使用老的样式
for ((j = ; j < ; j++))
do
boxCur[$j]=${boxTest[$j]}
done
fi
} #DrawCurBox(bDraw), 绘制当前移动中的方块, bDraw为1, 画上, bDraw为0, 抹去方块。
function DrawCurBox()
{
local i j t bDraw sBox s
bDraw=$ s=""
if (( bDraw == ))
then
sBox="\040\040"
else
sBox="[]"
s=$s"\033[1m\033[7m\033[3${cBoxCur}m\033[4${cBoxCur}m"
fi for ((j = ; j < ; j += ))
do
((i = iTrayTop + + ${boxCur[$j]} + boxCurY))
((t = iTrayLeft + + * (boxCurX + ${boxCur[$j + ]})))
#\[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 = ; j < ${#boxNew[@]}; j++))
do
boxCur[$j]=${boxNew[$j]}
done #显示当前移动的方块
if (( ${#boxCur[@]} == ))
then
#计算当前方块该从顶端哪一行"冒"出来
for ((j = , t = ; j < ; j += ))
do
if ((${boxCur[$j]} < t)); then t=${boxCur[$j]}; fi
done
((boxCurY = -t))
for ((j = , i = -, t = ; j < ; j += ))
do
if ((${boxCur[$j]} > i)); then i=${boxCur[$j]}; fi
if ((${boxCur[$j]} < t)); then t=${boxCur[$j]}; fi
done
((boxCurX = (iTrayWidth - - i - t) / )) #显示当前移动的方块
echo -ne `DrawCurBox ` #如果方块一出来就没处放,Game over!
if ! BoxMove $boxCurY $boxCurX
then
kill -$sigExit ${PPID}
ShowExit
fi
fi #清除右边预显示的方块
for ((j = ; j < ; j++))
do
((i = iTop + + j))
((t = iLeft + * iTrayWidth + ))
echo -ne "\033[${i};${t}H "
done #随机产生新的方块
((iBoxNewType = RANDOM % ${#offsetBox[@]}))
((iBoxNewRotate = RANDOM % ${countBox[$iBoxNewType]}))
for ((j = , i = (${offsetBox[$iBoxNewType]} + $iBoxNewRotate) * ; j < ; 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 = ; j < ; j += ))
do
((i = iTop + + ${boxNew[$j]}))
((t = iLeft + * iTrayWidth + + * ${boxNew[$j + ]}))
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 + ))
((t3 = iLeft + iTrayWidth * + ))
for ((i = ; i < iTrayHeight; i++))
do
((t1 = i + iTop + ))
echo -ne "\033[${t1};${t2}H||"
echo -ne "\033[${t1};${t3}H||"
done ((t2 = iTop + iTrayHeight + ))
for ((i = ; i < iTrayWidth + ; i++))
do
((t1 = i * + iLeft + ))
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 * + ))
((t2 = iTop + ))
echo -ne "\033[3${cScore}m\033[${t2};${t1}HScore"
((t2 = iTop + ))
echo -ne "\033[3${cScoreValue}m\033[${t2};${t1}H${iScore}"
((t2 = iTop + ))
echo -ne "\033[3${cScore}m\033[${t2};${t1}HLevel"
((t2 = iTop + ))
echo -ne "\033[3${cScoreValue}m\033[${t2};${t1}H${iLevel}"
echo -ne "\033[0m"
} #退出时显示GameOVer!
function ShowExit()
{
local y
((y = iTrayHeight + iTrayTop + ))
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 $ --show& #以参数--show将本程序再运行一遍
RunAsKeyReceiver $! #以上一行产生的进程的进程号作为参数
fi

"俄罗斯方块"使用shell编写的源码(这是我从网络上找到的代码,不得不说写着程序的哥们也太有才了~)

SHELL脚本编程基础知识