编程实践Linux / UNIX Shell编程极简教程
Posted 禅与计算机程序设计艺术
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了编程实践Linux / UNIX Shell编程极简教程相关的知识,希望对你有一定的参考价值。
不同于一般的介绍Linux Shell 的文章,本文并未花大篇幅去介绍 Shell 语法,而是以面向“对象” 的方式引入大量的实例介绍 Shell 日常操作,“对象” 涵盖数值、逻辑值、字符串、文件、进程、文件系统等。这样有助于学以致用,并在用的过程中提高兴趣。也可以作为 Shell 编程索引,在需要的时候随时检索。
1. 什么是 Shell ?
首先让我们从下图看看 Shell 在整个操作系统中所处的位置吧,该图的外圆描述了整个操作系统(比如 Debian/Ubuntu/Slackware 等),内圆描述了操作系统的核心(比如 Linux Kernel),而 Shell 和 GUI 一样作为用户和操作系统之间的接口。
GUI 提供了一种图形化的用户接口,使用起来非常简便易学;而 Shell 则为用户提供了一种命令行的接口,接收用户的键盘输入,并分析和执行输入字符串中的命令,然后给用户返回执行结果,使用起来可能会复杂一些,但是由于占用的资源少,而且在操作熟练以后可能会提高工作效率,而且具有批处理的功能,因此在某些应用场合还非常流行。
Shell 作为一种用户接口,它实际上是一个能够解释和分析用户键盘输入,执行输入中的命令,然后返回结果的一个解释程序(Interpreter,例如在 linux 下比较常用的 Bash),我们可以通过下面的命令查看当前的 Shell :
$ echo $SHELL
/bin/bash
$ ls -l /bin/bash
-rwxr-xr-x 1 root root 702160 2008-05-13 02:33 /bin/bash
该解释程序不仅能够解释简单的命令,而且可以解释一个具有特定语法结构的文件,这种文件被称作脚本(Script)。
既然该程序可以解释具有一定语法结构的文件,那么我们就可以遵循某一语法来编写它,它有什么样的语法,如何运行,如何调试呢?下面我们以 Bash 为例来讨论这几个方面。
搭建运行环境为了方便后面的练习,我们先搭建一个基本运行环境:在一个 Linux 操作系统中,有一个运行有 Bash 的命令行在等待我们键入命令,这个命令行可以是图形界面下的 Terminal (例如 Ubuntu 下非常厉害的 Terminator),也可以是字符界面的 Console (可以用 CTRL+ALT+F1~6 切换),如果你发现当前 Shell 不是 Bash,请用下面的方法替换它:
$ chsh $USER -s /bin/bash
$ su $USER
或者是简单地键入Bash:
$ bash
$ echo $SHELL # 确认一下
/bin/bash
如果没有安装 Linux 操作系统,也可以考虑使用一些公共社区提供的 Linux 虚拟实验服务,一般都有提供远程 Shell,你可以通过 Telnet 或者是 Ssh 的客户端登录上去进行练习。
有了基本的运行环境,那么如何来运行用户键入的命令或者是用户编写好的脚本文件呢 ?
假设我们编写好了一个 Shell 脚本,叫 test.sh 。
第一种方法是确保我们执行的命令具有可执行权限,然后直接键入该命令执行它:
$ chmod +x /path/to/test.sh
$ /path/to/test.sh
第二种方法是直接把脚本作为 Bash 解释器的参数传入:
$ bash /path/to/test.sh
或
$ source /path/to/test.sh
或
$ . /path/to/test.sh
基本语法介绍
先来一个 Hello, World 程序。
下面来介绍一个 Shell 程序的基本结构,以 Hello, World 为例:
#!/bin/bash -v
# test.sh
echo "Hello, World"
把上述代码保存为 test.sh,然后通过上面两种不同方式运行,可以看到如下效果。
方法一:
$ chmod +x test.sh
$ ./test.sh
./test.sh
#!/bin/bash -v
echo "Hello, World"
Hello, World
方法二:
$ bash test.sh
Hello, World
$ source test.sh
Hello, World
$ . test.sh
Hello, World
我们发现两者运行结果有区别,为什么呢?这里我们需要关注一下 test.sh 文件的内容,它仅仅有两行,第二行打印了 Hello, World,两种方法都达到了目的,但是第一种方法却多打印了脚本文件本身的内容,为什么呢?
原因在该文件的第一行,当我们直接运行该脚本文件时,该行告诉操作系统使用用#! 符号之后面的解释器以及相应的参数来解释该脚本文件,通过分析第一行,我们发现对应的解释器以及参数是 /bin/bash -v,而 -v 刚好就是要打印程序的源代码;但是我们在用第二种方法时没有给 Bash 传递任何额外的参数,因此,它仅仅解释了脚本文件本身。
Shell 程序设计过程Shell 语言作为解释型语言,它的程序设计过程跟编译型语言有些区别,其基本过程如下:
设计算法
用 Shell 编写脚本程序实现算法
运行脚本程序
可见它没有编译型语言的"麻烦的"编译和链接过程,不过正是因为这样,它出错时调试起来不是很方便,因为语法错误和逻辑错误都在运行时出现。下面我们简单介绍一下调试方法。
Shell 语言作为一门解释型语言,可以使用大量的现有工具,包括数值计算、符号处理、文件操作、网络操作等,因此,编写过程可能更加高效,但是因为它是解释型的,需要在执行过程中从磁盘上不断调用外部的程序并进行进程之间的切换,在运行效率方面可能有劣势,所以我们应该根据应用场合选择使用 Shell 或是用其他的语言来编程。
2 UNIX 程序设计
什么是 Shell Script 脚本 ?
Shell 是用户访问 Unix 操纵系统的接口。它接收用户的输入,然后基于该输入执行程序。程序执行完后,结果会显示在显示器上。
Shell 就是运行指令、程序和 Shell 脚本的运行环境。就和操作系统可以有很多种类一样,Shell 也有很多种。每一种 Shell 都有其特定的指令和函数集。
Shell 提示符
提示符 $
被称为命令提示符。当显示命令提示符后,用户就可以键入命令。
Shell 在用户按 Enter 键后,从用户输入设备读入输入信息,它通过查看用户输入的第一个单词,来获知用户想要执行的命令。一个字即使字符不分割组成的字符串,一般是空格和制表符分割字。
下面是在显示器上显示当前日期和时间的 date 指令的例子:
$date
Thu Jun 25 08:30:19 MST 2009
用户也可以定制自己喜欢的命令提示符,方法是改变环境变量 PS1。
Shell 类型
Unix 系统中有两种主要的 shell:
Bourne shell:如果用户使用 bourne shell,默认命令提示符是
$
。C shell:如果用户使用 bourne shell,默认命令提示符是
%
。
Bourne shell 也有如下几种子分类:
Bourne shell ( sh)
Korn shell ( ksh)
Bourne Again shell ( bash)
POSIX shell ( sh)
C shell不同的类型如下:
C shell ( csh)
TENEX/TOPS C shell ( tcsh)
最初的 UNIX Shell 是 Stephen R. Bourne 在 1970 年代中期写的。当时,他在新泽西的 AT&T 贝尔实验室工作。
Bourne shell是第一个出现在 Unix 系统中的 shell,因此它被称为标准的“shell”。
Bourne shell通常是安装在大多数版本的 Unix 中的 /bin/sh
目录。由于这个原因,在不同版本的 Unix 上也会选择这种 Shell 来编写脚本。
在本教程中,我们将覆盖 Bourne shell 中的大部分概念。
Shell 脚本
Shell 脚本的主要形式就是一系列的命令,这些命令会顺序执行。良好风格的 Shell 会有相应的注释。
Shell 脚本有条件语句(A 大于 B)、循环语句、读取文件和存储数据、读取变量且存储数据,当然,Shell 脚本也包括函数。
Shell 脚本和函数都是翻译型语言,所以他们并不会被编译。
在后面的部分,我们会尝试写一些脚本。他们是一些写有命令的简单文本文件。
脚本例子
假设我们创建一个名为 test.sh 的脚本。注意所有脚本的后缀名都必须为 .sh。假设之前,用户已经往里面添加了一些命令,下面就是要启动这个脚本。例子如下:
#!/bin/sh
这个命令告诉系统,后面的是 bourne shell它应念成 shebang,因为 # 被称为 hash,!称为 bang
为了创建包含这些指令的脚本,用户需要先键入 shebang 行,然后键入指令:
#!/bin/bash
pwd
ls
Shell 注释
可以像下面一样来为脚本添加注释:
#!/bin/bash
# Author : Zara Ali
# Copyright (c) Tutorialspoint.com
# Script follows here:
pwd
ls
现在用户已经保存了上述内容,然后就可以执行了:
$chmod +x test.sh
执行脚本方式如下:
$./test.sh
这会输出如下结果:
/home/amrood
index.htm unix-basic_utilities.htm unix-directories.htm
test.shunix-communication.htmunix-environment.htm
注意:如果想要执行当前目录下的脚本,需要使用如下方式 ./program_name
扩展的 Shell 脚本:
Shell 脚本有几个构造告诉 Shell 环境做什么和什么时候去做。当然,大多数脚本比上面复杂得多。
毕竟,Shell 是一种真正的编程语言,它可以有变量,控制结构等等。无论多么复杂的脚本,它仍然只是一个顺序执行的命令列表。
以下脚本使用 read 命令从键盘输入并分配给变量 PERSON,最后打印 STDOUT。
#!/bin/sh
# Author : Zara Ali
# Copyright (c) Tutorialspoint.com
# Script follows here:
echo "What is your name?"
read PERSON
echo "Hello, $PERSON"
下面是运行该脚本的例子:
$./test.sh
What is your name?
Zara Ali
Hello, Zara Ali
$
变量
变量就是被赋值后的字符串。那个赋给变量的值可以是数字、文本、文件名、设备或其他类型的数据。
本质上,变量就是执行实际数据的指针。Shell 可以创建、赋值和删除变量。
变量名
变量名仅能包含字母、数字或者下划线。
约定俗成的,UNIX Shell 的变量名都使用大写。
下面是一些有效的变量名的例子:
_ALI
TOKEN_A
VAR_1
VAR_2
下面是一些无效的变量名的例子:
2_VAR
-VARIABLE
VAR1-VAR2
VAR_A!
不能使用!
、*
、-
等字符的原因是,这些字符在 Shell 中有特殊用途。
定义变量
变量可以按照如下方式来定义:
variable_name=variable_value
比如:
NAME="Zara Ali"
上述例子定义了变量 NAME,然后赋值 "Zara Ali".这种类型的变量是常规变量,这种变量一次只能赋值一个。
Shell 可以随心所欲的赋值。比如:
VAR1="Zara Ali"
VAR2=100
访问变量
为了获取存储在变量内的值,需要在变量名前加 $.
比如,下面的脚本可以访问变量 NAME 中的值,然后将之打印到 STDOUT:
#!/bin/sh
NAME="Zara Ali"
echo $NAME
会出现下面的值:
Zara Ali
只读变量
Shell 使用只读命令提供了使变量只读化的功能。这样的变量,都不能被改变。
比如,下面的脚本中,对变量 NAME 的值进行修改,系统会报错:
#!/bin/sh
NAME="Zara Ali"
readonly NAME
NAME="Qadiri"
会出现如下结果:
/bin/sh: NAME: This variable is read only.
删除变量
变量的删除会告诉 Shell 从变量列表中删除变量从而,无法对其进行跟踪。一旦用户删除了一个变量,将无法访问存储在变量中。
下面是使用 unset 指令的例子:
unset variable_name
上述指令会取消已定义变量。下面是简单的例子:
#!/bin/sh
NAME="Zara Ali"
unset NAME
echo $NAME
上述例子不会显示任何信息,不能使用 unset 指令取消被标记为只读模式的变量。
变量类型
Shell 脚本被执行的时候,主要存在如下三种变量类型:
局部变量:该类型变量只会在当前 Shell 实例内有效。他们无法适用于由 Shell 启动的程序。他们仅在命令提示符处进行设置。
环境变量:环境变量对 Shell 的任何子进程都有效。部分程序是需要正确的调用函数才需要环境变量。通常,Shell 脚本只会定义程序运行需要的环境变量。
Shell 变量:该类型变量是由 Shell 设置的专用变量,是用来正确调用函数用的。有时这些变量是环境变量,有时是局部变量。
特殊变量
之前的教程就在命名变量时,使用某些非字符数值作为字符变量名提出警告。这是因为这些字符用于作为特殊的 UNIX 变量的名称。这些变量是预留给特定功能的。
例如,$ 字符代表进程的 ID 码,或当前 Shell 的 PID:
$echo $$
以上命令将输出当前 Shell 的 PID:
29949
下面的表列出了一些特殊变量,可以在你的 Shell 脚本中使用它们:
变量 | 描述 |
---|---|
$0 | 当前脚本的文件名。 |
$n | 这些变量对应于调用一个脚本时的参数。n 是一个十进制正整数,对应于特定参数的位置(第一个参数是 $1,第二个参数是 $2 等等)。 |
$# | 提供给脚本的参数数量。 |
$* | 所有的参数都表示两个引用。如果一个脚本接收了两个参数,即 $* 相当于 $1 $2。 |
$@ | 所有的参数都是两个单独地引用。如果一个脚本接收了两个参数,即 $@ 相当于 $1 $2。 |
$? | 执行最后一个命令的退出态。 |
$$ | 当前 shell 的进程号。对于 shell 脚本,即他们正在执行的进程的 ID。 |
$! | 最后一个后台命令的进程号。 |
命令行参数
命令行参数 $1,$2,$3,……$9 是位置参数,$0 指向实际的命令,程序,shell 脚本或函数。$1,$2,$3,……$9 作为命令的参数。
以下脚本使用与命令行相关的各种特殊变量:
#!/bin/sh
echo "File Name: $0"
echo "First Parameter : $1"
echo "Second Parameter : $2"
echo "Quoted Values: $@"
echo "Quoted Values: $*"
echo "Total Number of Parameters : $#"
这是一个运行上述脚本的示例:
$./test.sh Zara Ali
File Name : ./test.sh
First Parameter : Zara
Second Parameter : Ali
Quoted Values: Zara Ali
Quoted Values: Zara Ali
Total Number of Parameters : 2
特殊参数 $* 和 $@
存在一些特殊参数,使用它们可以访问所有的命令行参数。除非他们包含在双引号 "" 中,否则 $* 和 $@ 运行是相同的。
这两个参数都指定所有的命令行参数,但 $* 特殊参数将整个列表作为一个参数,各个值之间用空格隔开。而 $@ 特殊参数将整个列表分隔成单独的参数。
我们可以编写如下所示的 Shell 脚本,使用 $* 或 $@ 特殊参数来处理数量未知的命令行参数:
#!/bin/sh
for TOKEN in $*
do
echo $TOKEN
done
作为示例,运行上述脚本:
$./test.sh Zara Ali 10 Years Old
Zara
Ali
10
Years
Old
注意:这里 do……done 是一种循环,我们将在后续教程中介绍它。
退出态
$? 变量代表前面的命令的退出态。
退出态是每个命令在其完成后返回的数值。一般来说,大多数命令如果它们成功地执行,将 0 作为退出态返回,如果它们执行失败,则将 1 作为退出态返回。
一些命令由于一些特定的原因,会返回额外的退出状态。例如,一些命令为了区分不同类型的错误,将根据特定类型的失败原因返回各种不同的退出态值。
下面是一个成功命令的例子:
$./test.sh Zara Ali
File Name : ./test.sh
First Parameter : Zara
Second Parameter : Ali
Quoted Values: Zara Ali
Quoted Values: Zara Ali
Total Number of Parameters : 2
$echo $?
0
$
数组
一个 Shell 变量只能够容纳一个值。这种类型的变量称为标量变量。
Shell 数组变量可以同时容纳多个值,它支持不同类型的变量。数组提供了一种变量集分组的方法。你可以使用一个数组变量存储所有其他的变量,而不是为每个必需的变量都创建一个新的名字。
Shell 变量中讨论的所有命名规则都将适用于命名数组。
定义数组值
一个数组变量和一个标量变量之间的差异可以解释如下。
假如你想描绘不同学生的名字,你需要命名一系列变量名作为一个变量集合。每一个单独的变量是一个标量变量,如下所示:
NAME01="Zara"
NAME02="Qadir"
NAME03="Mahnaz"
NAME04="Ayan"
NAME05="Daisy"
我们可以使用一个数组来存储所有上面提到的名字。下面是创建一个数组变量的最简单的方法,将值赋给数组的一个索引。表示如下:
array_name[index]=value
这里 array_name 是数组的名称,index 是数组中需要赋值的索引项,value 是你想要为这个索引项设置的值。
例如,以下命令:
NAME[0]="Zara"
NAME[1]="Qadir"
NAME[2]="Mahnaz"
NAME[3]="Ayan"
NAME[4]="Daisy"
如果使用 ksh shell,数组初始化的语法如下所示:
set -A array_name value1 value2 ... valuen
如果使用 bash shell,数组初始化的语法如下所示:
array_name=(value1 ... valuen)
访问数组值
在为数组变量赋值之后,你可以访问它。如下所示:
$array_name[index]
这里 array_name 是数组的名称,index 是将要访问的值的索引。下面是一个最简单的例子:
#!/bin/sh
NAME[0]="Zara"
NAME[1]="Qadir"
NAME[2]="Mahnaz"
NAME[3]="Ayan"
NAME[4]="Daisy"
echo "First Index: $NAME[0]"
echo "Second Index: $NAME[1]"
这将产生以下结果:
$./test.sh
First Index: Zara
Second Index: Qadir
你可以使用以下方法之一,来访问数组中的所有项目:
$array_name[*]
$array_name[@]
这里 array_name 是你感兴趣的数组的名称。下面是一个最简单的例子:
#!/bin/sh
NAME[0]="Zara"
NAME[1]="Qadir"
NAME[2]="Mahnaz"
NAME[3]="Ayan"
NAME[4]="Daisy"
echo "First Method: $NAME[*]"
echo "Second Method: $NAME[@]"
这将产生以下结果:
$./test.sh
First Method: Zara Qadir Mahnaz Ayan Daisy
Second Method: Zara Qadir Mahnaz Ayan Daisy
基本操作符
每一种 Shell 都支持各种各样的操作符。我们的教程基于默认的 Shell(Bourne),所以在我们的教程中涵盖所有重要的 Bourne Shell 操作符。
下面列出我们将讨论的操作符:
算术运算符。
关系运算符。
布尔操作符。
字符串运算符。
文件测试操作符。
最初的 Bourne Shell 没有任何机制来执行简单算术运算,它使用外部程序 awk 或者最简单的程序 expr。
下面我们用一个简单的例子说明,两个数字相加:
#!/bin/sh
val=`expr 2 + 2`
echo "Total value : $val"
这将产生以下结果:
Total value : 4
注意以下事项:
操作符和表达式之间必须有空格,例如 2+2 是不正确的,这里应该写成 2 + 2。
完整的表达应该封闭在两个单引号 '' 之间。
算术运算符
下面列出 Bourne Shell 支持的算术运算符。
假设变量 a 赋值为 10,变量 b 赋值为 20:
运算符 | 描述 | 例子 |
---|---|---|
+ | 加法 - 将操作符两边的数加起来 | `expr $a + $b` = 30 |
- | 减法 - 用操作符左边的操作数减去右边的操作数 | `expr $a - $b` = -10 |
* | 乘法 - 将操作符两边的数乘起来 | `expr $a \\* $b` = 200 |
/ | 除法 - 用操作符左边的操作数除以右边的操作数 | `expr $b / $a` = 2 |
% | 取模 - 用操作符左边的操作数除以右边的操作数,返回余数 | `expr $b % $a` = 0 |
= | 赋值 - 将操作符右边的操作数赋值给左边的操作数 | a=$b 将 b 的值赋给了 a |
== | 相等 - 比较两个数字,如果相同,返回 true | [ $a == $b ] = false |
!= | 不相等 - 比较两个数字,如果不同,返回true。 | [ $a != $b ] = true |
这里需要非常注意是,所有的条件表达式和操作符之间都必须用空格隔开,例如 [$a == $b] 是正确的,而 [$a==$b] 是不正确的。
所有的算术计算都是针对长整数操作的。
关系运算符
Bourne Shell 支持以下的关系运算符,这些运算符是专门针对数值数据的。它们不会对字符串值起作用,除非他们的值是数值数据。
例如,下面的操作符将检查 10 和 20 之间的关系以及 “10” 和 “20” 的关系,但不能用于判断 “ten” 和 “twenty” 的关系。
假设变量 a 赋值为 10, 变量 b 赋值为 20:
运算符 | 描述 | 例子 |
---|---|---|
-eq | 检查两个操作数的值是否相等,如果值相等,那么条件为真。 | [ $a -eq $b ] is not true. |
-ne | 检查两个操作数的值是否相等,如果值不相等,那么条件为真。 | [ $a -ne $b ] is true. |
-gt | 检查左操作数的值是否大于右操作数的值,如果是的,那么条件为真。 | [ $a -gt $b ] is not true. |
-lt | 检查左操作数的值是否小于右操作数的值,如果是的,那么条件为真。 | [ $a -lt $b ] is true. |
-ge | 检查左操作数的值是否大于等于右操作数的值,如果是的,那么条件为真。 | [ $a -ge $b ] is not true. |
-le | 检查左操作数的值是否小于等于右操作数的值,如果是的,那么条件为真。 | [ $a -le $b ] is true. |
这里需要非常注意是,所有的条件表达式和操作符之间都必须用空格隔开,例如 [$a <= $b] 是正确的,而 [$a<=$b] 是不正确的。
布尔操作符
Bourne Shell 支持以下的布尔操作符。
假设变量 a 赋值为 10, 变量 b 赋值为 20:
运算符 | 描述 | 例子 |
---|---|---|
! | 这表示逻辑否定。如果条件为假,返回真,反之亦然。 | [ ! false ] is true. |
-o | 这表示逻辑 OR。如果操作对象中有一个为真,那么条件将会是真。 | [ $a -lt 20 -o $b -gt 100 ] is true. |
-a | 这表示逻辑 AND。如果两个操作对象都是真,那么条件才会为真,否则为假。 | [ $a -lt 20 -a $b -gt 100 ] is false. |
字符串运算符
Bourne Shell 支持以下字符串运算符。
假设变量 a 赋值为 "abc", 变量 b 赋值为 "efg":
示例说明
运算符 | 描述 | 例子 |
---|---|---|
= | 检查两个操作数的值是否相等,如果是的,那么条件为真。 | [ $a = $b ] is not true. |
!= | 检查两个操作数的值是否相等,如果值不相等,那么条件为真。 | [ $a != $b ] is true. |
-z | 检查给定字符串操作数的长度是否为零。如果长度为零,则返回true。 | [ -z $a ] is not true. |
-n | 检查给定字符串操作数的长度是否不为零。如果长度不为零,则返回true。 | [ -z $a ] is not false. |
str | 检查字符串str是否是非空字符串。如果它为空字符串,则返回false。 | [ $a ] is not false. |
文件测试操作符
下列操作符用来测试与Unix文件相关联的各种属性。
假设一个文件变量 file,包含一个文件名 "test",文件大小是100字节,在其上有读、写和执行权限:
示例说明
运算符 | 描述 | 例子 |
---|---|---|
-b file | 检查文件是否为块特殊文件,如果是,那么条件为真。 | [ -b $file ] is false. |
-c file | 检查文件是否为字符特殊文件,如果是,那么条件变为真。 | [ -c $file ] is false. |
-d file | 检查文件是否是一个目录文件,如果是,那么条件为真。 | [ -d $file ] is not true. |
-f file | 检查文件是否是一个不同于目录文件和特殊文件的普通文件,如果是,那么条件为真。 | [ -f $file ] is true. |
-g file | 检查文件是否有组ID(SGID)设置,如果是,那么条件为真。 | [ -g $file ] is false. |
-k file | 检查文件是否有粘贴位设置,如果是,那么条件为真。 | [ -k $file ] is false. |
-p file | 检查文件是否是一个命名管道,如果是,那么条件为真。 | [ -p $file ] is false. |
-t file | 检查文件描述符是否可用且与终端相关,如果是,条件为真实。 | [ -t $file ] is false. |
-u file | 检查文件是否有用户id(SUID)设置,如果是,那么条件为真。 | [ -u $file ] is false. |
-r file | 检查文件是否可读,如果是,那么条件为真。 | [ -r $file ] is true. |
-w file | 检查文件是否可写,如果是,那么条件为真。 | [ -w $file ] is true. |
-x file | 检查文件是否可执行,如果是,那么条件为真。 | [ -x $file ] is true. |
-s file | 检查文件大小是否大于0,如果是,那么条件为真。 | [ -s $file ] is true. |
-e file | 检查文件是否存在。即使文件是一个目录目录,只有存在,条件就为真。 | [ -e $file ] is true. |
决策
编写 Shell 脚本时,可能存在一种情况,你需要在两条路径中选择一条路径。所以你需要使用条件语句,确保你的程序做出正确的决策并执行正确的操作。
UNIX Shell 支持条件语句,这些语句基于不同的条件,用于执行不同的操作。在这里,我们将介绍以下两个决策语句:
if……else语句
case…… esac语句
if……else 语句:
if……else 语句是非常有用的决策语句,它可以用来从一个给定的选项集中选择一个选项。
Unix Shell 支持以下形式的 if……else 的语句:
if...fi statement
if...else...fi statement
if...elif...else...fi statement
大部分的 if 语句使用关系运算符检查关系,这部分知识在前一章已经讨论过。
case…… esac 语句
你可以使用多个 if……elif 语句执行一个多路分支。然而,这并不总是最好的解决方案,特别是当所有的分支都依赖于一个单一变量的值。
Unix Shell 支持 case……esac 语句,可以更确切地处理这种情况,它比重复 if……elif 语句更加有效。
case...esac
语句只有一种形式,详细说明如下:
case...esac statement
Unix Shell 的 case……esac 语句非常类似于 switch……case 语句,switch……case 语句在其他编程语言如 C 或 C++ 和 PERL 等中实现。
循环
循环是一个强大的编程工具,可以使您能够重复执行一系列命令。针对 Shell 程序员,有 4 种循环类型:
while 循环
for 循环
until 循环
select 循环
根据不同的情况使用不同的循环。例如只要给定条件仍然是 true,while 循环将执行给定的命令。而 until 循环是直到给定的条件变成 true,才会执行。
一旦你有了良好的编程实践,你就会开始根据情况使用适当的循环。while 循环和 for 循环在大多数其他编程语言如 C、C++ 和 PERL 等中都有实现。
嵌套循环
所有的循环都支持嵌套的概念,这意味着可以将一个循环放到另一个相似或不同的循环中。这个嵌套可以根据您的需求高达无限次。
下面是一个嵌套 while 循环的例子,基于编程的要求,其他循环类型也可以以类似的方式嵌套:
嵌套 while 循环
可以使用 while 循环作为另一个 while 循环体的一部分。
语法
while command1 ; # this is loop1, the outer loop
do
Statement(s) to be executed if command1 is true
while command2 ; # this is loop2, the inner loop
do
Statement(s) to be executed if command2 is true
done
Statement(s) to be executed if command1 is true
done
例子
下面是循环嵌套的一个简单例子,在循环内部添加另一个倒计时循环,用来数到九:
#!/bin/sh
a=0
while [ "$a" -lt 10 ]# this is loop1
do
b="$a"
while [ "$b" -ge 0 ] # this is loop2
do
echo -n "$b "
b=`expr $b - 1`
done
echo
a=`expr $a + 1`
done
这将产生以下结果。重要的是要注意 echo -n 是如何工作的。这里 -n
选项让输出避免了打印新行字符。
0
1 0
2 1 0
3 2 1 0
4 3 2 1 0
5 4 3 2 1 0
6 5 4 3 2 1 0
7 6 5 4 3 2 1 0
8 7 6 5 4 3 2 1 0
9 8 7 6 5 4 3 2 1 0
循环控制
到目前为止你已经学习过创建循环以及用循环来完成不同的任务。有时候你需要停止循环或跳出循环迭代。
在本教程中你将学到以下语句用于控制 Shell 循环:
break 语句
continue 语句
无限循环
所有循环都有一个有限的生命周期。当条件为假或真时它们将跳出循环,这取决于这个循环。
一个循环可能会由于未匹配到适合得条件而无限执行。一个永远执行没有终止的循环会执行无数次。因此,这种循环被称为无限循环。
例子
这是一个使用 while 循环显示数字 0 到 9 的简单的例子:
#!/bin/sh
a=10
while [ $a -ge 10 ]
do
echo $a
a=`expr $a + 1`
done
这个循环将永远持续下去,因为 a 总是大于或等于 10,它永远不会小于 10。所以这正是无限循环的一个恰当的例子。
break 语句
所有在 break 语句之前得语句执行结束后执行 break 语句,break 语句用于跳出整个循环。然后执行循环体后面的代码。然后在循环结束后运行接下来的代码。
语法
以下 break 语句将用于跳出一个循环:
break
break 语句也可以使用这种格式来退出嵌套循环式:
break n
在这里 n 指定封闭循环执行的次数然后退出循环。
例子
这里是一个简单的例子,用来说明只要 a 变成 5 循环将终止:
#!/bin/sh
a=0
while [ $a -lt 10 ]
do
echo $a
if [ $a -eq 5 ]
then
break
fi
a=`expr $a + 1`
done
这会产生以下结果:
0
1
2
3
4
5
这里是一个简单的嵌套 for 循环的例子。如果 var1 等于 var2 以及 var2 等于 0 ,则这个脚本将跳出这个双重循环:
#!/bin/sh
for var1 in 1 2 3
do
for var2 in 0 5
do
if [ $var1 -eq 2 -a $var2 -eq 0 ]
then
break 2
else
echo "$var1 $var2"
fi
done
done
这会产生以下结果。在内循环中,有一个 break 命令,其参数为 2。这表明,你应该打破外循环和内循环才能满足条件。
1 0
1 5
continue 语句
continue 语句类似于 break 命令,二者不同之处在于,continue 语句用语结束当前循环,能引起当前循环的迭代的退出,而不是整个循环。
这个语句在当程序发生了错误,但你想执行下一次循环的时候是非常有用的。
语法
continue
正如 break 语句,一个整型参数可以传递给 continue 命令以从嵌套循环中跳过命令。
continue n
在这里 n 指定封闭循环执行的次数然后进入下一次循环。
例子
下面是使用 continue 语句的循环,它返回 continue 语句并且开始处理下一个语句:
#!/bin/sh
NUMS="1 2 3 4 5 6 7"
for NUM in $NUMS
do
Q=`expr $NUM % 2`
if [ $Q -eq 0 ]
then
echo "Number is an even number!!"
continue
fi
echo "Found odd number"
done
这会产生以下结果:
Found odd number
Number is an even number!!
Found odd number
Number is an even number!!
Found odd number
Number is an even number!!
Found odd number
替代
什么是替代?
当它遇到包含一个或多个特殊字符的表达式时 Shell 执行替代。
例
以下是一个例子,在这个例子中,变量被其真实值所替代。同时,“\\n” 被替换为换行符:
#!/bin/sh
a=10
echo -e "Value of a is $a \\n"
这会产生以下结果。在这里 -e 选项可以解释反斜杠转义。
Value of a is 10
下面是没有 -e 选项的结果:
Value of a is 10\\n
这里有以下转义序列可用于 echo 命令:
Escape | Description |
---|---|
\\\\ | 反斜杠 |
\\a | 警报(BEL) |
\\b | 退格键 |
\\c | 抑制换行 |
\\f | 换页 |
\\n | 换行 |
\\r | 回车 |
\\t | 水平制表符 |
\\v | 垂直制表符 |
默认情况下,你可以使用 -E 选项来禁用反斜线转义解释。
你你可以使用 -n 选项来禁用换行的插入。
命令替换:一种机制,通过它, Shell 执行给定的命令,然后在命令行替代他们的输出。
语法
当给出如下命令时命令替换就会被执行:
command
当执行命令替换确保你使用反引号,而不是单引号字符。
例
命令替换一般是用来分配一个命令的输出变量。下面的示例演示命令替换:
#!/bin/sh
DATE=`date`
echo "Date is $DATE"
USERS=`who | wc -l`
echo "Logged in user are $USERS"
UP=`date ; uptime`
echo "Uptime is $UP"
这会产生以下结果:
Date is Thu Jul 2 03:59:57 MST 2009
Logged in user are 1
Uptime is Thu Jul 2 03:59:57 MST 2009
03:59:57 up 20 days, 14:03, 1 user, load avg: 0.13, 0.07, 0.15
变量代换
变量代换使 Shell 程序员操纵基于状态变量的值。
下面的表中是所有可能的替换:
Form | Description |
---|---|
$var | 替代 var 的值 |
$var:-word | 如果 var 为空或者没有赋值,word 替代 var。var 的值不改变。 |
$var:=word | 如果 var 为空或者没有赋值, var 赋值为 word 的值。 |
$var:?message | 如果 var 为空或者没有赋值,message 被编译为标准错误。这可以检测变量是否被正确赋值。 |
$var:+word | 如果 var 被赋值,word 将替代 var。var 的值不改变。 |
例
以下是例子用来说明上述替代的各种状态:
#!/bin/sh
echo $var:-"Variable is not set"
echo "1 - Value of var is $var"
echo $var:="Variable is not set"
echo "2 - Value of var is $var"
unset var
echo $var:+"This is default value"
echo "3 - Value of var is $var"
var="Prefix"
echo $var:+"This is default value"
echo "4 - Value of var is $var"
echo $var:?"Print this message"
echo "5 - Value of var is $var"
这会产生以下结果:
Variable is not set
1 - Value of var is
Variable is not set
2 - Value of var is Variable is not set
3 - Value of var is
This is default value
4 - Value of var is Prefix
Prefix
5 - Value of var is Prefix
引用机制
元字符
UNIX Shell 提供有特殊意义的各种元字符,同时利用他们在任何 Shell 脚本,并导致终止一个字,除了引用。
举个例子,在列出文件中的目录时 ? 匹配一个一元字符,并且 * 匹配多个字符。下面是一个 Shell 特殊字符(也称为元字符)的列表:
* ? [ ] ' " \\ $ ; & ( ) | ^ < > new-line space tab
在一个字符前使用 \\ ,它可能被引用(例如,代表它自己)。
例子
下面的例子,显示了如何打印 * 或 ? :
#!/bin/sh
echo Hello; Word
这将产生下面的结果。
Hello
./test.sh: line 2: Word: command not found
shell returned 127
现在,让我们尝试使用引用字符:
#!/bin/sh
echo Hello\\; Word
这将产生以下结果:
Hello; Word
$ 符号是一个元字符,所以它必须被引用,以避免被 Shell 特殊处理:
#!/bin/sh
echo "I have \\$1200"
这将产生以下结果:
I have $1200
是以下四种形式的引用:
Quoting | Description |
---|---|
单引号 | 所有在这些引号之间的特殊字符会失去它们特殊的意义 |
双引号 | 所有在这些引号之间的特殊字符会失去它们特殊的意义除了以下字符: - $ - ` - \\$ - \\' - \\" -\\\\ |
反斜杠 | 任何直接跟在反斜杠后的字符会失去它们特殊的意义 |
反引号 | 所有在这些引号之间的特殊字符会被当做命令而被执行 |
单引号
考虑包含许多特殊的 Shell 字符的 echo 命令:
echo <-$1500.**>; (update?) [y|n]
在每个特殊字符前加反斜杠会显得异常繁琐,并且不容易阅读:
echo \\<-\\$1500.\\*\\*\\>\\; \\(update\\?\\) \\[y\\|n\\]
有一个简单的方法来引用一大组字符。将一个单引号(')放在字符串的开头和结尾:
echo '<-$1500.**>; (update?) [y|n]'
单引号内的任何字符被引用正如每个字符前均加上一个反斜杠。所以,现在这个 echo 命令将正确地显示。
如果要输出的一个字符串内出现一个单引号,你不应该把整个字符串置于单引号内,相反你应该在单引号前使用反斜杠(\\)如下:
echo 'It\\'s Shell Programming'
双引号
尝试执行以下 Shell 脚本。这个 Shell 脚本使用了单引号:
VAR=ZARA
echo '$VAR owes <-$1500.**>; [ as of (`date +%m/%d`) ]'
这将输出以下结果:
$VAR owes <-$1500.**>; [ as of (`date +%m/%d`) ]
所以这不是你想显示的内容。很明显,单引号防止变量替换。如果想替换的变量值和如预期那样使引号起作用,那么就需要把命令放置在双引号内,如下:
VAR=ZARA
echo "$VAR owes <-\\$1500.**>; [ as of (`date +%m/%d`) ]"
这将产生以下结果:
ZARA owes <-$1500.**>; [ as of (07/02) ]
除以下字符外,双引号使所有字符的失去特殊含义:
$ 参数替代。
用于命令替换的反引号。
\\$ 使美元标志在字面上显示。
\\` 使反引号在字面上显示。
\\" 启用嵌入式双引号。
\\ 启用嵌入式反斜杠。
所有其他\\字符在字面上显示(而不是特殊意义)。
单引号内的任何字符被引用正如每个字符前均加上一个反斜杠。所以,现在这个 echo 命令将正确地显示。
如果要输出的字符串内出现一个单引号,你不应该把整个字符串置于单引号内,相反你应该在单引号前使用反斜杠(\\)如下:
echo 'It\\'s Shell Programming'
反引号
置于反引号之间的任何 Shell 命令将执行命令
语法
下面是一个简单的语法,把任何 Shell 命令置于反引号之间:
例子
var=`command`
例子
下面将执行 date 命令,产生的结果将被存储在 DATA 变量中。
DATE=`date`
echo "Current Date: $DATE"
这将输出以下结果:
Current Date: Thu Jul 2 05:28:45 MST 2009
输入/输出重定向
大多数 UNIX 系统命令从你的终端接受输入并将所产生的输出发送回到您的终端。一个命令通常从一个叫标准输入的地方读取输入,默认情况下,这恰好是你的终端。同样,一个命令通常将其输出写入到标准输出,默认情况下,这也是你的终端。
输出重定向
一个命令的输出通常用于标准输出,也可以很容易地将输出转移到一个文件。这种能力被称为输出重定向:
如果记号 > file
添加到任何命令,这些命令通常将其输出写入到标准输出,该命令的输出将被写入文件,而不是你的终端:
检查下面的 who 命令,它将命令的完整的输出重定向在用户文件中。
$ who > users
请注意,没有输出出现在终端中。这是因为输出已被从默认的标准输出设备(终端)重定向到指定的文件。如果你想检查 users 文件,它有完整的内容:
$ cat users
oko tty01 Sep 12 07:30
ai tty15 Sep 12 13:32
ruthtty21 Sep 12 10:10
pat tty24 Sep 12 13:07
steve tty25 Sep 12 13:03
$
如果命令输出重定向到一个文件,该文件已经包含了一些数据,这些数据将会丢失。考虑这个例子:
$ echo line 1 > users
$ cat users
line 1
$
您可以使用 >>
运算符将输出添加在现有的文件如下:
$ echo line 2 >> users
$ cat users
line 1
line 2
$
输入重定向
正如一个命令的输出可以被重定向到一个文件中,所以一个命令的输入可以从文件重定向。大于号 >
被用于输出重定向,小于号 <
用于重定向一个命令的输入。
通常从标准输入获取输入的命令可以有自己的方式从文件进行输入重定向。例如,为了计算上面 user 生成的文件中的行数,你可以执行如下命令:
$ wc -l users
2 users
$
在这里,它产生的输出为 2 行。你可以通过从 user 文件进行 wc 命令的标准输入重定向:
$ wc -l < users
2
$
请注意,两种形式的 wc 命令产生的输出是有区别的。在第一种情况下,用行数列出该文件的用户的名称,而在第二种情况下,它不是。
在第一种情况下,wc 知道,它是从文件用户读取输入。在第二种情况下,只知道它是从标准输入读取输入,所以它不显示文件名。
Here 文档
here document 被用来将输入重定向到一个交互式 Shell 脚本或程序。
在一个 Shell 脚本中,我们可以运行一个交互式程序,无需用户操作,通过提供互动程序或交互式 Shell 脚本所需的输入。
Here 文档的一般形式是:
command << delimiter
document
delimiter
这里的 Shell 将 <<
操作符解释为读取输入的指令,直到它找到含有指定的分隔符线。然后所有包含行分隔符的输入行被送入命令的标准输入。
分隔符告诉 Shell here 文档已完成。没有它,Shell 不断的读取输入。分隔符必须是一个字符且不包含空格或制表符。
以下是输入命令 wc -l 来进行计算行的总数:
$wc -l << EOF
This is a simple lookup program
for good (and bad) restaurants
in Cape Town.
EOF
3
$
可以用 here document 编译多行,脚本如下:
#!/bin/sh
cat << EOF
This is a simple lookup program
for good (and bad) restaurants
in Cape Town.
EOF
这将产生以下结果:
This is a simple lookup program
for good (and bad) restaurants
in Cape Town.
下面的脚本用 vi 文本编辑器运行一个会话并且将输入保存文件在 test.txt 中。
#!/bin/sh
filename=test.txt
vi $filename <<EndOfCommands
i
This file was created automatically from
a shell script
^[
ZZ
EndOfCommands
如果用 vim 作为 vi 来运行这个脚本,那么很可能会看到以下的输出:
$ sh test.sh
Vim: Warning: Input is not from a terminal
$
运行该脚本后,你应该看到以下内容添加到了文件 test.txt 中:
$ cat test.txt
This file was created automatically from
a shell script
$
丢弃输出
有时你需要执行命令,但不想在屏幕上显示输出。在这种情况下,你可以通过重定向到文件 /dev/null
以丢弃输出:
$ command > /dev/null
在这里 command 是要执行的命令的名字。文件 /dev/null
是一个自动丢弃其所有的输入的特殊文件。
为了丢弃一个命令的输出和它的错误输出,你可以使用标准重定向来将 STDOUT 重定向到 STDERR :
$ command > /dev/null 2>&1
在这里,2 代表 STDERR , 1 代表 STDOUT。可以上通过将 STDERR 重定向到 STDERR 来显示一条消息,如下:
$ echo message 1>&2
重定向命令
以下是可以用来重定向的命令的完整列表:
命令 | 描述 |
---|---|
pgm > file | pgm 的输出被重定向到文件 |
pgm < file | pgm 程序从文件度它的输入 |
pgm >> file | pgm 的输出被添加到文件 |
n > file | 带有描述符 n 的输出流重定向到文件 |
n >> file | 带有描述符 n 的输出流添加到文件 |
n >& m | 合并流 n 和 流 m 的输出 |
n <& m | 合并流 n 和 流 m 的输入 |
<< tag | 标准输入从开始行的下一个标记开始。 |
| | 从一个程序或进程获取输入,并将其发送到另一个程序或进程。 |
需要注意的是文件描述符 0 通常是标准输入(STDIN),1 是标准输出(STDOUT),2 是标准错误输出(STDERR)。
函数
函数允许你将一个脚本的整体功能分解成更小的逻辑子部分,然后当需要的时候可以被调用来执行它们各自的任务。
使用函数来执行重复性的任务是一个创建代码重用的很好的方式来。代码重用是现代面向对象编程的原则的重要组成部分。
Shell 函数类似于其他编程语言中的子程序和函数。
创建函数
声明一个函数,只需使用以下语法:
function_name ()
list of commands
函数的名字是 function_name,在脚本的其它地方你可以用函数名调用它。函数名后必须加括号,在其后加花括号,其中包含了一系列的命令。
例子
以下是使用函数的简单例子:
#!/bin/sh
# Define your function here
Hello ()
echo "Hello World"
# Invoke your function
Hello
当你想执行上面的脚本时,它会产生以下结果:
$./test.sh
Hello World
$
函数的参数传递
你可以定义一个函数,在调用这些函数的时候可以接受传递的参数。这些参数可以由 $1,$2 等表示。
以下是一个例子,我们传递两个参数 Zara 和 Ali ,然后我们在函数中捕获和编译这些参数。
#!/bin/sh
# Define your function here
Hello ()
echo "Hello World $1 $2"
# Invoke your function
Hello Zara Ali
这将产生以下结果:
$./test.sh
Hello World Zara Ali
$
函数返回值
如果你从一个函数内部执行一个 exit 命令,不仅能终止函数的执行,而且能够终止调用该函数的 Shell 程序。
如果你只是想终止该函数的执行,有一种方式可以跳出定义的函数。
根据实际情况,你可以使用 return 命令从你的函数返回任何值,其语法如下:
return code
这里的 code 可以是你选择的任何东西,但很明显,考虑到将脚本作为一个整体,你应该选择有意义的或有用的东西。
例子
下面的函数返回一个值 1:
#!/bin/sh
# Define your function here
Hello ()
echo "Hello World $1 $2"
return 10
# Invoke your function
Hello Zara Ali
# Capture value returnd by last command
ret=$?
echo "Return value is $ret"
这将产生以下结果:
$./test.sh
Hello World Zara Ali
Return value is 10
$
嵌套函数
函数更有趣的功能之一就是他们可以调用本身以及调用其他函数。调用自身的函数被称为递归函数。
下面简单的例子演示了两个函数的嵌套:
#!/bin/sh
# Calling one function from another
number_one ()
echo "This is the以上是关于编程实践Linux / UNIX Shell编程极简教程的主要内容,如果未能解决你的问题,请参考以下文章