摘要
- 什么是Shell
- Shell的分类
- 在linux上搭建Shell编程环境
- Shell 如何执行
- 交互式程序
- 创建脚本
- Shell脚本的参数
- Shell脚本的基本元素
- 指定命令解读器
- Shell脚本中的注释和风格
- 如何执行Shell程序
- Shell程序的退出状态
- 变量和引用
- 变量的命令
- 变量的类型
- 变量和引号
- 变量的作用域
- 系统变量
- 环境变量
- 变量赋值
- 引用变量的值
- 清除变量
- 引用符号
- 命令替换
- 条件测试
- 条件测试
- 字符串测试
- 整数测试
- 文件测试
- 逻辑操作符
- 条件判断语句
- 多条件判断语句case
- 运算符
- 执行算术运算
- 位运算符
- 自增/自减运算符
- 循环结构
- 带列表的for循环语句
- 类C风格的for循环语句
什么是Shell
在Linux系统中,Shell是用户与系统内核之间进行交互的接口。
Shell这个单词的意思是“外壳”,它形象地表达出了Shell的作用。在UNIX以及Linux中,Shell就是套在内核外面的一层外壳。正因为有Shell的存在,才向普通的用户隐藏了许多关于系统内核的细节。
Shell又称命令解释器,它能识别用户输入的各种命令,并传递给操作系统。它的作用类似于Windows操作系统中的命令行,但是,Shell的功能远比命令行强大得多。在UNIX或者Linux中,Shell既是用户交互的界面,也是控制系统的脚本语言。
Shell的分类
关于Shell的分类,在介绍Shell的历史的时候已经简单地介绍过一些了,下面对各种Shell程序做一个简单的概括。常见的几种Shell程序如下所述。
- BourneShell:标识为sh,该Shell由SteveBourne在贝尔实验室时编写。在许多UNIX系统中,该Shell是root用户的默认的Shell。
- BourneAgainShell:标识为bash,该Shell由BrianFox在1987年编写,是绝大多数Linux发行版的默认的Shell。
- KornShell:标识为ksh,该Shell由贝尔实验室的DavidKorn在二十世纪八十年代早期编写。它完全向上兼容BourneShell并包含了CShell的很多特性。
- C Shell:标识为csh,该Shell由BillJoy在BSD系统上开发。由于其语法类似于C语言,因此称为CShell。
对于这些Shell程序,其语法或多或少都有所区别。目前大部分人仍然建议使用标准的BourneAgainShell。
Shell不仅仅是充当用户与UNIX或者Linux交互界面的角色,还可以作为一种程序设计语言来使用。通过Shell编程,可以实现许多非常实用的功能,提高系统管理的自动化水平。
在linux上搭建Shell编程环境
由于Linux本身都会默认安装Shell脚本的运行环境,所以通常情况下,并不需要用户额外安装什么软件。
在同一台Linux上面会同时安装多个Shell,并且,这些Shell的语法会有所不同,所以,用户在编写和执行Shell脚本的时候一定要弄清楚当前使用的是哪种Shell。用户可以使用系统变量$SHELL来获取当前系统默认的Shell。
从上面的输出结果可以得知,当前系统默认的Shell为bash。
我们都是指定使用的Shell为/bin/sh。实际上,在Linux中,这是一个指向/bin/bash的符号链接,如下:
这意味着,尽管我们在程序中指定的解释器为/bin/sh,但是实际上解释Shell脚本的是/bin/bash。
Shell作为一个软件包,当然也有版本,用户可以使用如下命令来查看bash的版本:
Shell 如何执行
Shell脚本程序有以下两种执行方式:
(1)用户可以依次输入一系列的命令,交互式地执行它们;
(2)用户也可以把所有的这些命令按照顺序保存在一个文件中,然后将该文件作为一个程序来执行。
交互式程序
在命令行上直接输入命令来交互式地执行Shell脚本是一种非常简单的方式。尤其是在测试Shell程序的时候,通过使用交互式方式,可以非常方便地得到程序执行的结果。
例:在当前目录中查找文件名包含“xml”这3个字符的文件。如果找到的话,则在当前屏幕上打印出来。我们可以在Shell提示符后面依次输入下面的代码:
01 [root@linux~]# for filename in `ls .` 02 > do 03 > if echo "$filename" | grep "xml" 04 > then 05 > echo "$filename" 06 > fi 07 > done 08 package.xml 09 package.xml 10 wbxml1.0.3 11 wbxml1.0.3 12 wbxml1.0.3.tgz 13 wbxml1.0.3.tgz
命令行直接执行功能相同:
for filename in `ls .`; do if echo "$filename" | grep "xml"; then echo "$filename"; fi; done
创建脚本
对于一组需要经常重复执行的Shell语句来说,将它们保存在一个文件中来执行是一种非常明智的做法。我们通常称这种包含多个Shell语句的文件为Shell脚本,或者Shell脚本文件。脚本文件都是普通的文本文件,可以使用任何的文本编辑器查看或者修改Shell脚本文件。
02 #! /bin/sh
03
04 # for 循环开始
05 for filename in `ls .`
06 do
07 #如果文件名包含xml
08 if echo "$filename" | grep "xml"
09 then
10 #输出文件名
11 echo "$filename"
12 fi
13 done
从上面的代码可以得知,Shell程序中的注释以“#”符号开始,一直持续到该行的结束。请注意第一行#!/bin/sh,它是一种特殊形式的注释,其中,“#!”字符告诉系统同一行中紧跟在它后面的那个参数是用来执行本文件的程序。在这个例子中,/bin/sh是默认的Shell程序。当将脚本编辑完成之后,这个脚本还不能马上执行,把脚本设置为可执行。
Shell脚本的参数
从命令行传递给Shell脚本的参数又称为位置参数,这主要是因为Shell脚本会根据参数的位置来接收它们的值。在Shell脚本内部,用户可以通过一系列的系统变量来获取参数。这些变量的名称都是固定的,并且非常简单,只用1个字符表示,例如$0表示当前执行的脚本名称,$1表示传递给脚本的第1个参数等。
注:
由单引号或者双引号引起来的字符串作为一个参数进行传递,传递时会去掉引号。
对于包含空白字符或者其他的特殊字符的参数,需要使用单引号或者双引号进行传递。
变量$@可以以“参数1”“参数2”“参数3”……的形式返回所有的参数的值,因此,$@与“$1”“$2”“$3”……是等价的。如果用户传递的参数中包含空格或者其他的特殊字符,需要使用$@来获取所有的参数的值,不能使用$*。
变量$*以“参数1参数2参数3……”的形式将所有的参数作为一个字符串返回。通常情况下,参数值之间通过空格、制表符或者换行符来隔开,在默认情况下使用空格。
变量$#返回传递给脚本的参数的数量,不包括$0,即排除脚本的名称。
另外,如果用户传递的参数多于9个,则不能使用$10来引用第10个参数。为了能够获取第10个参数的值,用户必须处理或保存第1个参数,即$1,然后使用shift命令删除参数1并将所有剩余的参数下移1位,此时$10就变成了$9,依此类推。$#的值将被更新以反映参数的剩余数量。
参数扩展
用户需要编写一个脚本程序,并且这个脚本程序需要一个拥有许多值的参数,在程序中,用户希望根据这个参数的值来执行不同的操作。在这种情况下,单纯地依靠$1以及$2等变量已经不能满足需求了。此时,用户可以考虑使用参数扩展。
为了获取到这些参数的值,用户需要在Shell程序中使用getopts命令。
Shell脚本的基本元素
第2行,指定命令解读器:“#!/bin/bash”。
第4行,注释:说明某些代码的功能。
第5行,可执行语句:实现程序的功能。
指定命令解读器
通常会同时安装多个Shell程序,例如sh、bash或者csh等。而这些不同的Shell程序的语法会有些区别,那么到底使用哪个Shell来执行代码呢?
当用户在命令行中执行该程序时,当前的Shell会载入该程序的代码,并且读取其中的第2行,如果发现有“#!”标识,则表示当前的程序指定了解释并执行它的Shell。然后会尝试读取“#!”标识后面的内容,搜寻解释器的绝对路径。如果发现了指定的解释器,则会创建一个关于该解释器的进程,解释并执行当前脚本的语句。
Shell脚本的这个规定使得用户可以非常灵活地调用任何解释器,而不仅仅限于Shell程序。下面介绍如何在脚本文件中指定其他解释器程序。
在PHP脚本文件中指定PHP语言的解释器,然后执行文件中的PHP代码,代码如下:
Shell脚本中的注释和风格
通过在代码中增加注释可以提高程序的可读性。传统的Shell只支持单行注释,其表示方法是一个井号“#”,从该符号开始一直到行尾都属于注释的内容,如果需要多行注释内容的话,则在每行注释的开头都要加上“#”。
但是这并不意味着用户只能使用单行注释。实际上,用户还可以通过其他一些变通的方法来实现多行注释,其中,最简单的方法就是使用冒号“:”配合heredocument,其语法如下:
:<<BLOCK ....注释内容 BLOCK
如何执行Shell程序
可以通过3种方式来实现。这3种方式分别为:
- 授予用户执行该脚本文件的权限,使得该程序能够直接执行。
- 通过调用Shell脚本解释器来执行。
- 通过source命令来执行。
关于第一种方法,前面已经详细介绍过了,不再重复说明。第二种方式就是将脚本文件作为参数传递给解释器,在通过这种方式执行脚本的时候,不需要用户拥有执行该脚本文件的权限,只要拥有读取该文件的权限即可。
source命令是一个Shell内部命令,其功能是读取指定的Shell程序文件,并且依次执行其中所有的语句。
Shell程序的退出状态
在UNIX或者Linux中,每个命令都会返回一个退出状态码。退出状态码是一个整数,其有效范围为0~255。通常情况下,成功的命令返回0,而不成功的命令返回非0值。非0值通常都被解释成一个错误码。
同样,Shell脚本中的函数和脚本本身也会返回退出状态码。在脚本或者是脚本函数中执行的最后的命令会决定退出状态码。另外,用户也可以在脚本中使用exit语句将指定的退出状态码传递给Shell。
在前面的所有的例子中,我们都没有通过exit语句退出程序。在这种情况下,整个程序的退出状态码由最后执行的那一条语句来决定,另外,在Shell中,系统变量$?保存了最后一条命令的退出状态。
程序的退出状态非常重要,它反映了脚本的执行是否成功。用户可以根据脚本的执行状态来决定下一步的操作。
在上面的代码中,第4行是一个正常的echo语句,因此第6行的输出结果应该是0。第8行是一个无效的命令,因此第10行会输出一个非0值,具体是什么值要看当前Shell的设置。第11行是一个正常的echo语句,同样该语句的退出状态也是0。第13行通过exit语句将退出状态码120返回给当前的Shell。
变量和引用
变量的命令
在Shell中,变量名可以由字母、数字或者下划线组成,并且只能以字母或者下划线开头,大小写是敏感的。
变量的类型
根据变量类型确定的时间,可以将程序设计语言分为两类,分别是静态类型语言和动态类型语言。其中,静态类型语言是在程序的编译期间就确定变量类型的语言,例如Java、C++和PASCAL,在这些语言中使用变量时,必须首先声明其类型。动态设计语言是在程序执行过程中才确定变量的数据类型的语言。常见的动态语言有VBScript、PHP及Python等。在这些语言中,变量的数据类型根据第一次赋给该变量的值的数据类型来确定。
同样,根据是否强制要求类型定义,可以将程序设计语言分为强类型语言和弱类型语言。强类型语言要求用户在定义变量时必须明确指定其数据类型,例如Java和C++。在强类型语言中,数据类型之间的转换非常重要。与之相反,弱类型语言则不要求用户明确指定变量的数据类型,例如VBScript。用户可以将任意类型的数值赋给该变量。并且,变量的数据类型之间的转换也无需明确进行。
Shell是一种动态类型语言和弱类型语言,即在Shell中,变量的数据类型无需显示地声明,变量的数据类型会根据不同的操作有所变化。准确地讲,Shell中的变量是不分数据类型的,统一地按照字符串存储。
在Shell中,通常情况下用户可以直接使用变量,无需先进行定义,当用户第一次使用某个变量名时,实际上就同时定义了这个变量,在变量的作用域内,用户都可以使用该变量。
尽管通过以上方式可以非常方便地定义变量,但是,对于变量的某些属性却不容易控制,例如,变量的类型和读写属性等。为了更好地控制变量的相关属性,bash提供了一个名称为declare的命令来声明变量,该命令的基本语法如下:
以上程序的执行结果如下:
下面对比执行结果分析一下代码。
第5行使用通常的方法定义了一个变量x,并且将一个算术式作为初始值赋给该变量。第6行输出变量x的值。前面已经讲过,Shell中将所有的数据都看做是字符串来存储的,所以在程序执行的时候,Shell并不将6/3当成一个将被求值的算术式,而是作为一个普通的字符串,所以第6行直接输出了这个算术式本身,得到了结果的第2行。
代码的第8行使用declare语句声明了变量x为整数,但是程序并没对变量x重新赋值,所以第9行的echo语句的执行结果仍然得到算术式本身,即结果的第3行。
代码的第11行对变量x重新赋值,将前面的算术表达式赋给它。因为当变量被声明为整数之后,可以直接参与算术运算,所以第12行的echo语句中输出了算术式的值,即结果的第4行。
第14行尝试将一个字符串值赋给整数变量x,并且在第15行使用echo语句输出x的值。在Shell中,如果变量被声明成整数,把一个结果不是整数的表达式赋值给它时,就会变成0。因此,结果第5行中的0是代码第15行的echo语句的输出。
第17行将一个浮点数赋给变量x,因为bash并不内置对浮点数的支持,所以得到了执行结果中的第6行的错误消息,此时,变量x的值变为0,即结果的第7行中的0。
第20行取消变量x的整数类型属性,第22行重新将算术式赋给变量x,并且在第23行使用echo语句输出变量x的值。由于此时变量x已经不是整数变量,所以不能直接参数算术运算。因此,变量x的值仍然得到了算术式本身,即结果的第8行。
在Shell中,为了得到算术式的值,可以有两种方法,其中一种就是使用方括号,即第25行中的方式。结果的第9行正是此时变量x的值。另外一种是使用圆括号,即第28行中的方式。从执行结果可以得知,这两种方式都可以得到用户所期望的结果。
第31行使用r选项声明了一个只读变量,第43行尝试为该变量重新赋值,从而得到了结果中第12行的错误消息。此时变量x的值仍然是2,所以才有结果中的第13行。
变量和引号
Shell变量中的符号“$”表示取变量的值。只有在取值的时候才使用,定义和赋值时无需使用符号“$”。另外,实际上Shell中变量的原型为${var},而常用的书写形式$var只是一个简写。在某些情况下,简写形式会导致程序执行错误。
Shell语言中一共有3种引号,分别为单引号('')、双引号("")和反引号(``)。这3种引号的作用是不同的,其中单引号括起来的字符都作为普通字符出现;由双引号括起来的字符,除“$”、“\”、“'”和“"”这几个字符仍是特殊字符并保留其特殊功能外,其余字符仍作为普通字符对待;由反引号括起来的字串被Shell解释为命令,在执行时,Shell首先执行该命令,并以它的标准输出结果取代整个反引号(包括两个反引号)部分。
变量的作用域
1、全局变量
通常认为,全局变量是使用范围较大的变量,它不仅限于某个局部使用。在Shell语言中,全局变量可以在脚本中定义,也可以在某个函数中定义。在脚本中定义的变量都是全局变量,其作用域为从被定义的地方开始,一直到Shell脚本结束或者被显式地删除。
除了在脚本中定义全局变量之外,在函数内部定义的变量默认情况下也是全局变量,其作用域为从函数被调用时执行变量定义的地方开始,一直到Shell脚本结束或者被显式地删除为止。
2、局部变量
与全局变量相比,局部变量的使用范围较小,通常仅限于某个程序段访问,例如函数内部。在Shell语言中,可以在函数内部通过local关键字定义局部变量,另外,函数的参数也是局部变量。
系统变量
环境变量
按照惯例,Shell中的环境变量全部使用大写字母表示。
演示通过环境变量来获取与当前Shell有关的一些环境变量的值,代码如下:
变量赋值
一个没有初始化的Shell变量被认为是一个空字符串。用户可以通过变量的赋值操作来完成变量的声明并赋予一个特定的值。并且可以通过赋值语句为一个变量多次赋值,以改变其值。
一般情况下,Shell中将所有普通变量的值都看做字符串。如果value中包含空格、制表符和换行符,则必 须用单引号或者双引号将其引起来。双引号内允许变量替换,而单引号则不可以。中间的等于号“=” 称为赋值符号,赋值符号的左右两边不能直接跟空格,否则 Shell会将其视为命令。
引用变量的值
在 Shell 中,用户可以通过在变量名前面加上“$”来获取该变量的值。例如:$str。
为了能够使 Shell正确地界定变量名,避免混淆,用户在引用变量时可以使用大括号将变量名括起来。例如:${str}abc。
清除变量
使用unset语句:unset varicable_name
引用符号
命令替换
所谓命令替换, 是指在 Shell 程序中,将某个 Shell 命令的执行结果赋给某个变量。在bash中,有两种语法可以进行命令替换, 分别使用反引号和圆括号, 如下:
`shell_command` 或者 $(shell_command)
条件测试
在Shell程序中,用户可以使用测试语句来测试指定的条件表达式的条件的真或者假。当指定的条件为真时,整个条件测试的返回值为0;反之,如果指定的条件为假,则条件测试语句的返回值为非0值。
注意:在Shell程序中,条件测试中的指定条件为真时,条件测试的返回值为0。这主要是为了保持与Shell程序的退出状态一致。当某个Shell程序成功执行后,该进程会返回一个0值;而如果该程序执行错误,则会返回一个非0值。
条件测试的语法有两种,分别是test命令和[命令,下面对这两种语法进行介绍。
test命令的语法如下:
test expression
[命令的语法如下:
[ expression ]
字符串测试
整数测试
、对于初学者来说,经常犯的一个错误就是错误地使用运算符。在进行整数比较的时候,一定要是整数的运算符。但是,由于受到其他程序设计语言的影响,初学者可能会使用字符串运算符中的“=”和“!=”来进行整数比较。
文件测试
逻辑操作符
条件判断语句
在Shell程序中,如果想要将多条命令放在同一行中,则需要使用分号将其隔开。
当我们在Shell程序中使用代码创建或者修改某个文件的时候,首先判断一下文件是否创建成功,或者判断文件是否存在是一个非常好的习惯。
在Shell中,还有一个特殊的命令,称为空命令,其表示方法是一个冒号“:”,该命令不做任何事情,但是它的退出状态永远是0。因此,如果我们将该命令作为if语句中的条件,则会永远执行then子句,如下面的例子所示。
在实际的编程中,有人喜欢使用&&操作符来代替if语句,如下面的例子所示。
多条件判断语句case
在上面的语法中,variable是一个变量,case语句会将该变量的值与value1~valuen中的每个值相比较,如果与某个value的值相等,则执行该value所对应的一组语句。当遇到“;;”符号时,就跳出case语句,执行esac语句后面的语句。如果没有任何一个值与variable的值相匹配,则执行*后面的一组语句。
对于上面的case语句,用户应该注意以下几点:
- 对于变量名variable,可以使用双引号,也可以不使用。
- 每个case子句中的条件测试部分,都以右括号“)”结束。
- 每个case子句都以一对分号“;;”作为结束符。在脚本执行的过程中,当遇到一对分号时,会跳过当前case子句后面的所有的case子句,包括*所对应的子句,执行esac子句后面的其他的语句。
- case语句结构以esac结尾。这与if语句以fi结尾是一样的,都是以前面一个单词的所有字母的逆序排列作为结束标记
运算符
执行算术运算
可以通过4种方式来执行算术运算,这4种方式分别如下:
1、使用expr外部程序
expr expression
2、使用$((…))
3、使用$[…]
4、使用let命令
使用let命令可以执行一个或者多个算术表达式,其中的变量名无需使用$符号。如果表达式中含有空格或者其他特殊字符,则必须将其引用起来。
位运算符
自增/自减运算符
循环结构
带列表的for循环语句
在上面的语法中,variable称为循环变量,list是一个列表,可以是一系列的数字或者字符串,元素之间使用空格隔开。do和done之间的所有的语句称为循环体,即循环结构中重复执行的语句。
除了将各个数字全部列出之外,用户还可以使用另外一种比较简单的书写方法,即用一个范围来代替列出所有的元素。例如,上面的1~8所有的数字可以使用{1...8}来代替。
for语句的步长,即循环变量每次增加的值都是1。实际上,Shell允许用户指定for语句的步长。当用户需要另外指定步长时,其基本语法如下:
演示通过for循环,并配合步长来计算100以内奇数的和,代码如下:
在for循环的列表条件中,除了使用数字作为元素之外,还可以使用字符串。
本例将1周中7天的名字作为列表条件,依次输出每天的名称。代码如下:
如果使用字符串作为列表元素,实际上可以省略外面的大括号。因此,可以修改为以下形式:
在前面的例子中的for循环里面,都是直接指定循环的条件列表。实际上,Shell中的for循环非常灵活,除了直接指定条件之外,还可以通过其他的方式来获得条件列表。例如,某些Shell命令会输出一个列表。其中,最常见的命令就是ls命令,该命令可以列出某个目录下面的文件清单。
列表for循环还有一个比较重要的用途就是处理脚本的参数。前面讲过,用户可以通过系统变量“$*”一次获取所有的参数,并且,这些参数值之间是通过空格隔开的,所以,用户同样可以将该系统变量获取的参数值作为for循环的条件列表,从而可以依次处理各个参数。
类C风格的for循环语句
在Linux或者UNIX上面,C或者C++是最主流的开发语言。因此,从事Linux系统管理的系统管理员也或多或少地接触过C或者C++语言。通常来说,这些人对于C语言的语法比较熟悉。为了适应这部分用户的习惯,bash也提供了类C风格的for循环语句。类C风格的for循环语句的基本语法如下:
演示类C风格的for循环语句的使用方法,代码如下:
使用for循环语句处理数组
演示通过for循环来遍历数组,代码如下:
参考:
1、图书《Shell从入门到精通-张春晓》