Gentoo Logo

声明: 本文的原始版本最初发表于IBM developerWorks,现在所有权归属Westtech Information Services。本文档是原始文档的更新版本,包含了Gentoo Linux文档团队所做的很多改进。
现在无人积极维护本文档。


Bash示例,第一部分

内容:

1.  Bourne again shell(bash)基础编程

简介

你可能会奇怪为什么你应该学习Bash编程。那么,这里有两个令人信服的理由:

你已经开始运行它了

如果检查一下,你可能将会发现你现在正在运行bash。即使你改变了你默认的shell,bash也可能仍在你系统的某个地方运行,因为它是标准的Linux shell并被用做多种用途。由于bash已经运行,你运行的任何附加的bash脚本拥有固有的内存高效性,这是因为它们和任何已经运行的bash进程共享内存。如果你已经运行了些东西来做这项工作,并且做的很好,那还为什么要载入一个500K的解释器呢?

你正在使用它

不仅是因为你已经运行了bash,另外你还在每天的基本工作中与bash进行交互。它就在那里,所以学习如何最大限度的使用它就变得很有意义。这么做将是你的bash之旅更加有意思和具有创制性。但是为什么你应该学习bash编程呢?很简单,因为你已经在思考运行命令,复制粘贴文件,以及管道和重定向输出。那你是否应该学习这种语言,以便使用和利用那些已熟悉和喜爱的强大省时的概念呢?命令shell开启了UNIX系统的潜能,而bash就是Linux shell。它是你和机器之间的高等级的纽带。随着你的bash知识的增长,同时你将能自动的增加你在Linux和UNIX下的生产力──就是这么简单。

Bash的困惑

通过错误的方法学习bash会是一个非常困惑的过程。很多新手敲入man bash来查看bash的man页,但令人头疼的是只面临一些shell函数特别简明和科技性的解释。其他人通过敲入info bash(来查看GNU的信息文档),但可能是man页的翻版,也可能(如果他们幸运的话)仅是一些稍微友好的信息文档出现。

然而这些对于初学者来说在一定程度上是令人沮丧的,标准的bash文档不可能是适合于每一个人的所有东西和迎合所有已经对一般的shell编程十分熟悉的人。毋庸置疑的,在man页上有很多优秀的技术性的信息,但是它对于初学者来说帮助是有限的。

这是这个系列存在的原因。在它里面,我将向你展示如何有效的使用bash编程结构,以便你能够编写你自己的脚本。与科技性的说明不同的是,我将通过易懂的英文向你们讲解,使你不仅知道这是什么,而且知道你应该在什么时候有效的使用它。到这个三部分的系列之后,你将能够编写你自己的精巧的bash脚本,并能够达到一个能够很舒服的使用bash和通过读(和理解)标准的bash文档来增进你的知识的层次上。那就让我们开始吧!

环境变量

在bash和几乎所有其他shell下,用户可以定义环境变量,它们被以ASCII字符的形式存储在内部。环境变量最有用的地方在于它们是UNIX作业模型中的一个标准部分。这意味着环境变量并非是shell脚本所独有,而是同样可以为标准的编译程序所使用。当我们在bash下"export"一个环境变量,任何之后我们运行的程序都可以读取我们的设定,而不管它是否是不是一个shell脚本。一个很好的例子是vipw命令,它通常允许root去编辑系统密码文件。通过设定EDITER环境变量来命名你喜欢的文本编辑器,你可以设定vipw去使用而不是vi,特别是如果你习惯于xemacs而又实在讨厌vi。

在bash下定义一个环境变量的标准方法是:

代码 1.1: 定义环境变量

$ myvar='This is my environment variable!'

上面的命令定义了一个叫"myvar"的环境变量并包含"This is my environment variable!"字符串。上面有很多是需要我们注意的:首先,在"="号两侧没有空格,任何空格都将导致一个错误产生(可以试一下看看)。第二要注意的是虽然在定义一个词时我们可以省略引号,但当环境变量的值多于一个词(包含空格或制表符)时,引号却是必须的。

注意: 要想获得关于如何在bash中使用引号的更加详尽的信息,请参考bash man页中"QUOTING"一节。特殊字符序列由其它值"扩展"(替换)确实使 bash 中字符串的处理变得复杂。本系列将只讲述最常用的引用功能。

第三,当我们通常可以用双引号代替单引号,在上面的例子中这样做会导致一个错误。为什么呢?因为用单引号禁用了一个bash称为扩展的特性,其中特殊字符和字符序列的值可以互换。比如,"!"字符是历史扩展字符,它通常由之前敲入的命令所替换。(在这个系列的文章中,我们不能讲述历史扩展,那是在我们bash编程中不常用到它。要获得相关的更多信息,请看bash man页中的"HISTORY EXPANSION"部分。)尽管这个类似于宏的功能很便利,但我们现在只想在环境变量后面加上一个简单的感叹号,而不是宏。

现在让我们看一下一个人如何运用环境变量。下面是例子:

代码 1.2: 运用环境变量

$ echo $myvar
This is my environment variable!

通过在我们的环境变量前加上一个$,我们可以使bash来用myvar的值来取代它。在bash的术语中,这被成为"变量扩展"。但是,如果我们试过下面的会怎样:

代码 1.3: 首先使用变量扩展

$ echo foo$myvarbar
foo

我们希望这能echo "fooThis is my environment variable!bar",但是它却没有起作用。哪里出错了呢?简单的说,bash的变量扩展很容易让人迷惑。它不能判别我们要扩展的变量是$m,$my,$myvar,$myvarbar等等。我们如何才能明确的清楚的告诉bash我们提到的是哪个变量呢?请试一下这个:

代码 1.4: 合适的变量扩展

$ echo foo${myvar}bar
fooThis is my environment variable!bar

你可以看到,当环境变量名没有很清晰的和周围的文本分开时,我们可以把它们放在一对括号内。然而$myvar可以很快的打出来并在大部分时候都能工作,${myvar}在几乎所有的情况下都能正确的解析出来。除此之外,它们都做同样的事情,在以下的这个系列中这两种形式的变量扩展你将都能看到。当你的环境变量没有和你周围的文本用空白(空格或制表符)分开时,你将希望记住使用更加复杂的带括号的形式。

还记的我们还提到过我们可以"导出"变量。当我们导出一个环境变量时,它将自动的可以在任何随后执行或可执行的脚本中使用。Shell脚本可以运用shell的内建环境变量支持"获取"环境变量,而C程序可以运用getenv()函数来调用。这里是一些你应该敲入并编译的C代码的例子──它都可以使我们从C的视角来理解环境变量:

代码 1.5: myvar.c──一个C程序环境变量的例子

#include <stdio.h>
#include <stdlib.h>

int main(void) {
  char *myenvvar=getenv("EDITOR");
  printf("The editor environment variable is set to %s\n",myenvvar);
}

保存上面的源代码到一个叫myenv.c的文件,然后通过发出下面指令编译它:

代码 1.6: 编译上面的源代码

$ gcc myenv.c -o myenv

现在在你的文件夹里将有一个可执行的程序,当你运行它时,如果有,将打印出EDITOR的环境变量值。这是当我在我的机器上运行它时的结果:

代码 1.7: 运行上面的程序

$ ./myenv
The editor environment variable is set to (null)

Hmmm...因为环境变量EDITOR没有设定任何值,C程序得到的是一个空字符串。让我们试着将它设为一个特定的值:

代码 1.8: 用特定的值试验

$ EDITOR=xemacs
$ ./myenv
编辑器的环境变量被设为(空)

然而你可能会希望myenv会打印值"xemacs",它并非很管用,因为我们没有exportEDITOR环境变量。这次,我们将让它起作用:

代码 1.9: 同样的程序在导出变量后

$ export EDITOR
$ ./myenv
编辑器的环境变量被设为xemacs

因此,你已经用自己的眼睛看到另外一种方法(正如我们的C程序例子)直到环境变量被导出才能看到它。顺便一提,如果你喜欢,你可以用一行定义和输出一个环境变量,就像下面一样:

代码 1.10: 用一个命令定义和导出环境变量

$ export EDITOR=xemacs

它的工作和两行的版本是一样的。这是一个好的时机去展示如何用unset去除一个环境变量。

代码 1.11: 去除变量

$ unset EDITOR
$ ./myenv

拆分字符串综述

拆分字符串──它是将原始字符串拆分成小一点的,切片──是一个在你平常每天的shell脚本执行中的任务。很多时候,shell脚本需要采用全限定路径,并找到结束的文件或目录。虽然可以用bash编码实现(而且有趣),但标准basename UNIX可执行程序可以极好地完成此工作:

代码 1.12: 运用basename

$ basename /usr/local/share/doc/foo/foo.txt
foo.txt
$ basename /usr/home/drobbins
drobbins

basename是一个拆分字符串的及其简便的工具。与它相关的被称为dirname返回basename丢弃的"另"一部分路径。

代码 1.13: 运用dirname

$ dirname /usr/local/share/doc/foo/foo.txt
/usr/local/share/doc/foo
$ dirname /usr/home/drobbins/
/usr/home

注意: dirnamebasename都不考虑磁盘上的任何文件和文件夹;它们是纯粹的字符串操作命令。

命令替换

需要知道一个很简便的操作,如何创建一个包含可执行命令结果的环境变量。这是很容易做的:

代码 1.14: 创建一个包含命令结果的环境变量

$ MYDIR=`dirname /usr/local/share/doc/foo/foo.txt`
$ echo $MYDIR
/usr/local/share/doc/foo

我们上面所做的被称为命令替换。在这个例子中有很多值得注意的。在第一行,我们简单的将命令用反引号括起。这些不是标准的单引号,而是通常在键盘上位于Tab键之上的单引号。我们用bash的交互命令语法来做同样的事情:

代码 1.15: 交互命令替换语法

$ MYDIR=$(dirname /usr/local/share/doc/foo/foo.txt)
$ echo $MYDIR
/usr/local/share/doc/foo

正如我们所见,bash提供了很多执行相同操作的方法。运用命令替换,我们可以将任何命令和命令管道放在` `$( )之间并把它指定为环境变量。多方便啊!这里是一个如何在命令替换中使用管道的例子:

代码 1.16: 管道命令替换

$ MYFILES=$(ls /etc/| grep pa)
$ echo $MYFILES
pam.d passwd

像个专家一样拆分字符串

尽管basenamedirname是很棒的工具,但我们可能时常需要更加专业的字符串"拆分"操作而不是仅仅的标准的路径名处理。当我们需要更强的说服力时,我们可以利用bash内建的变量扩展功能。我们已经使用了标准的变量扩展,就像:${MYVAR}。但是bash还可以自己执行一些便利的字符串拆分。请看一下这些例子:

代码 1.17: 字符串拆分示例

$ MYVAR=foodforthought.jpg
$ echo ${MYVAR##*fo}
rthought.jpg
$ echo ${MYVAR#*fo}
odforthought.jpg

在第一个例子中,我们敲入${MYVAR##*fo}。那么它确切的意思是什么呢?基本上,在${ }中,我们敲入环境变量的名字,两个##和一个通配符("*fo")。然后,bash获取MYVAR,从字符串"foodforthought.jpg"的起始处查找匹配通配符"*fo"的最长的子串并将其从字符串的开始处拆分。刚开始这有一点难以掌握,为了感受一下这个特殊的"##"选项是如何工作的,让我们一步步探讨bash是如何完成这个扩展的。首先,它从"foodforthought.jpg的开始处开始搜索匹配"*fo"通配符的子串。这里是它检查到的子串:

代码 1.18: 子串检查

f       
fo              MATCHES *fo
foo     
food
foodf           
foodfo          MATCHES *fo
foodfor
foodfort        
foodforth
foodfortho      
foodforthou
foodforthoug
foodforthought
foodforthought.j
foodforthought.jp
foodforthought.jpg

寻找到匹配的字符串后,你可以看到bash找到两个。它选择了最长匹配,从原始字符串的开始处出去,然后返回结果。

上面的第二种形式的变量扩展和第一种表现的完全相同,只是它只运用一个"#"──并且bash执行了一个几乎相同的过程。正如我们的第一个例子一样,它检查乐同样的一些子串,除了bash从我们的原始字符串中移除了最短匹配并返回结果。所以,它一检查到"fo"子串,它就从我们的字符串中移除"fo"并返回"odforthought.jpg"。

这看起来很神秘,所以我们将向你展示一个简单的方法去记住这个功能。当搜索最长匹配时,运用##(因为##比#要长)。当搜索最短匹配时,用#。看,也并不是那么难去记忆!等等,怎样记住应该使用'#'字符来从字符串起始处移除呢?很简单!注意一下在US键盘上,shift-4是"$",它是bash变量扩展字符。在键盘上,紧靠在"$"左边是"#"。所以,你可以看到"#"是在"$"的"起始处的",因此(根据我们的记忆法),"#"从字符串的起始处移除字符。你可能还想知道如何从字符串的末尾移除字符。如果猜到我们用的字符是US键盘上紧靠在"$"右边的"%",正确!这里有几个简单的例子来解释如何拆分字符串的末尾部分:

代码 1.19:

$ MYFOO="chickensoup.tar.gz"
$ echo ${MYFOO%%.*}
chickensoup
$ echo ${MYFOO%.*}
chickensoup.tar

正如你所见,除了将匹配通配符从字符串末尾去除之外,%和%%变量扩展的选项和#和##没有什么不同。注意:如果你希望从末尾移除一个特殊的子串,则不能运用"*"字符:

代码 1.20: 从末尾移去字符

MYFOOD="chickensoup"
$ echo ${MYFOOD%%soup}
chicken

在这个例子中,既然只有一个匹配,我们使用"%%"或"%"也就不重要了。同时记住,如果你忘记了是使用"#"还是"%",请看键盘上的3,4,5键并猜出来。

我们可以运用其它形式的变量扩展来选择一个特殊的子串,它是基于一个特定的字符串偏移和长度。请尝试在bash下敲入下面几行:

代码 1.21: 选择一个特殊的子字符串

$ EXCLAIM=cowabunga
$ echo ${EXCLAIM:0:3}
cow
$ echo ${EXCLAIM:3:7}
abunga

这种形式的字符串拆分将十分简便;只需简单的用冒号分开指定字符的起始字符和子串的长度。

应用字符串拆分

现在我们已经学了拆分字符串的所有内容,让我们写一个简单的小shell脚本。我们的脚本将接受单个文件作为参数并打印出它是否是一个压缩档,它将在文件的结尾寻找模式".tar"。下面就是:

代码 1.22: mytar.sh──一个示例脚本

#!/bin/bash

if [ "${1##*.}" = "tar" ]
then
       echo This appears to be a tarball.
else
       echo At first glance, this does not appear to be a tarball.
fi

要运行这个脚本,将它输入到几个叫mytar.sh,并敲入chmod 755 mytar.sh使它可执行。然后如下给他一个压缩档做试验:

代码 1.23: 尝试这个脚本

$ ./mytar.sh thisfile.tar
This appears to be a tarball.
$ ./mytar.sh thatfile.gz
At first glance, this does not appear to be a tarball.

好,它工作了,但是它不是很实用。在我们将它变得实用之前,先让我们看看上面用的"if"语句。其中,我们有一个布尔表达式。在bash中,"="比较操作检查字符串是否相等。但是这布尔表达式真正测试的是什么呢?让我们看看左边。通过我们所学习的字符串拆分,"${1##*.}"将从包含环境变量"1"的字符串起始处移除"*."的最长匹配,并返回结果。这将返回文件中最后一个"."之后的所有部分。很明显,如果一个文件以".tar"结尾,我们将获取"tar"作为结果,条件也就为真。

你可能会吃惊环境变量"1"是在第一个位置。非常简单──$1是这个脚本的第一个命令行参数,$2是第二个,如此等等。好的,现在我们已经回顾乐这个函数,我们可以看一下"if"语句了。

If语句

像大多语言一样,bash有它自己的条件类型。当使用它们时,请准照上面的格式;它是使"if"和"then"在不同的行,"else"和结束处必须的"fi"和它们水平对齐。这使代码易于阅读和调试。除了"if,else"形式,"if"语句还有很多其它形式:

代码 1.24: if语句的基本结构

if      [ condition ]
then
        action
fi

只有当condition为真的,它才会执行一个动作,否则它不执行任何动作并继续执行直到"fi"的行。

代码 1.25: 在继续fi之后的命令之前检查状况

if [ condition ]
then
        action
elif [ condition2 ]
then
        action2
.
.
.
elif [ condition3 ]
then

else
        actionx
fi

上面的"elif"序列将连续测试每一个condition并执行第一个真的condition所相应的动作。如果没有condition是真,它将执行"else"动作,如果有一个为真,它将继续执行所有接下来的"if,elif,else"语句。

下一次

现在我们已经涵盖了大部分的基本的bash功能,是时候加快脚步准备书写一些真正的脚本了。在下一部分中,我将讲述循环结构,函数,命名空间和其它重要的话题。接下来我们将准备写更多的复杂的脚本。在第三篇文章中,我们将重点集中在非常复杂的脚本和函数上,正如很多bash脚本设计选项一样。到时候再见!

2.  资源

有用的链接



打印

更新于2013年 1月 27日

总结: 通过学习如何运用bash脚本语言进行编程,你每天和linux的交互将变的更有意思和有生产力,同时你也将能够构建那些你所知和喜欢的标准UNIX概念(诸如管道和重定向)。在这三部分的系列中,Daniel Robbins将教你如何通过bash例程进行编程。他将讲述非常基本的内容(这使它成为入门者优秀的系列)并在后续的系列中引入更加高级的特征。

Daniel Robbins
作者

苏永恒
译者

Donate to support our development efforts.

Copyright 2001-2014 Gentoo Foundation, Inc. Questions, Comments? Contact us.