Shell、脚本、编程和编译 1

来源:百度文库 编辑:神马文学网 时间:2024/04/27 17:07:33
LPI 102 考试准备,主题 109: Shell、脚本、编程和编译
初级管理(LPIC-1)主题 109

第 2 页,共 6 页
对本教程的评价

帮助我们改进这些内容
Shell 定制
本节介绍了初级管理(LPIC-1)考试 102 的 1.109.1 主题的内容。这个主题的权值为 5。
在本节中,我们将学习如何: 设置并取消环境变量 使用配置文件在登录或派生新 shell 时设置环境变量 对经常使用的命令序列编写 shell 函数 使用命令列表
在出现图形界面之前,程序员都是使用打字机终端或 ASCII 显示终端连接到 UNIX® 系统的。用户可以使用打字机终端输入命令,输出结果通常会被打印到连续的纸张上。大部分 ASCII 显示终端都是每行 80 个字符,每屏 25 行,不过也有比这更大或更小的终端。程序员输入一条命令并按下回车键之后,系统就会解释并执行这条命令。
尽管在当今这个使用拖拽式图形界面的时代,这一切看起来似乎太过原始,但是与原来编写程序、打卡、对卡迭(card deck)进行汇编并运行程序的方式相比,这已经是非常大的一个进步了。随着编辑器的出现,程序员甚至可以作为卡像来创建程序,并在终端会话中编译程序。
在终端中输入的字节流向 shell 提供了一个标准输入流,shell 返回的字符流可以打印到纸上,也可以显示到标准输出 上。
接受并执行命令的程序称为 shell。它位于您和操作系统之间。UNIX shell 和 Linux shell 的功能都非常强大,可以通过组合一些基本的函数来构造非常复杂的操作。通过使用编程结构则可以构建一些函数在 shell 中直接执行,或者将这些函数保存成 shell 脚本 的形式,这样就可以一次次重用这些函数了。
有时需要在系统引导之前就执行一些命令,以便能够进行终端连接;有时又需要周期性地执行命令,而不管您登录与否。shell 可以为您完成这些功能。标准输入和输出并不需要来自于(或定向到)终端处的真实用户。
在本节中,将学习更多有关 shell 的内容。具体来说,您将学习有关 bash(又称为 Bourne again)shell 的内容,它是对原来 Bourne shell 的一个增强,另外还提供了其他 shell 所具有的一些特性,以及对 Bourne shell 所做的一些更改以使其更加兼容 POSIX。
POSIX 是 Portable Operating System Interface for uniX 的简称,它是一系列 IEEE 标准,总称为 IEEE 1003。这些标准中的第一个标准是 IEEE Standard 1003.1-1988,它是在 1988 年发布的。其他知名的 shell 包括 Korn shell(ksh)、C shell(csh)及其派生产品 tcsh、Almquist shell(ash)及其 Debian 派生产品(dash)。一些脚本常常需要用到上述某个 shell 的特性,所以要对这些 shell 有一些了解。
您与计算机的很多交互特性在这些会话中都是相同的。回想一下在教程 “LPI 101 考试准备(主题 103):GNU 和 UNIX 命令” 中,当使用 Bash shell 时,就拥有了一个 shell 环境,它定义了很多内容,例如提示符格式、主目录、工作目录、shell 名、已经打开的文件、已经定义的函数等。每个 shell 进程都可以使用这个环境。shell(包括 bash)让您可以创建并修改 shell 变量,并可以将其导出 到环境中由在 shell 中运行的其他进程或从当前 shell 中派生的其他 shell 使用。
环境变量和 shell 变量都有名称。您可以通过在变量名前加上一个 ‘$‘ 符号来引用变量的值。一些常用的 bash 变量如表 3 所示。
表 3. 常用 bash 环境变量 变量名 功能
USER 已登录用户的用户名
UID 已登录用户的数字用户 id
HOME 用户的主目录
PWD 当前工作目录
SHELL shell 名
$ 进程 id(或正在运行的 Bash shell 进程或其他进程的 PID)
PPID 启动这个进程的进程的进程 id (即父进程的 id)
? 上一个命令的退出码




回页首
在 Bash shell 中,可以通过在一个名字后面紧跟上一个等号(=)来创建或设置 shell 变量。变量名(或标识符)是由字符、数字和下划线构成的单词,它只能由字符或下划线开头。变量是大小写敏感的,例如 var1 和 VAR1 是不同的两个变量。按照惯例,变量 —— 尤其是导出后的变量 —— 都采用大写,不过这并不是硬性要求。通常,$$ 和 $? 是 shell 参数,而不是变量。它们只能被引用;无法对它们进行赋值。
在创建 shell 变量时,通常都会希望将该变量导出 到环境中,这样从这个 shell 中启动的其他进程也都可以使用该变量了。但所导出的变量对父 shell 不可用。可以使用 export 命令导出一个变量名。在 bash 中,可以在一个步骤中完成赋值和导出。
为了展示赋值和导出操作,让我们在 Bash shell 中运行 bash 命令,然后在这个新 Bash shell 中在运行 Korn shell(ksh)。我们会使用 ps 命令来显示有关正在运行的命令的信息。
[ian@echidna ian]$ ps -p $$ -o "pid ppid cmd" PID PPID CMD 30576 30575 -bash [ian@echidna ian]$ bash [ian@echidna ian]$ ps -p $$ -o "pid ppid cmd" PID PPID CMD 16353 30576 bash [ian@echidna ian]$ VAR1=var1 [ian@echidna ian]$ VAR2=var2 [ian@echidna ian]$ export VAR2 [ian@echidna ian]$ export VAR3=var3 [ian@echidna ian]$ echo $VAR1 $VAR2 $VAR3 var1 var2 var3 [ian@echidna ian]$ echo $VAR1 $VAR2 $VAR3 $SHELL var1 var2 var3 /bin/bash [ian@echidna ian]$ ksh $ ps -p $$ -o "pid ppid cmd" PID PPID CMD 16448 16353 ksh $ export VAR4=var4 $ echo $VAR1 $VAR2 $VAR3 $VAR4 $SHELL var2 var3 var4 /bin/bash $ exit $ [ian@echidna ian]$ echo $VAR1 $VAR2 $VAR3 $VAR4 $SHELL var1 var2 var3 /bin/bash [ian@echidna ian]$ ps -p $$ -o "pid ppid cmd" PID PPID CMD 16353 30576 bash [ian@echidna ian]$ exit [ian@echidna ian]$ ps -p $$ -o "pid ppid cmd" PID PPID CMD 30576 30575 -bash [ian@echidna ian]$ echo $VAR1 $VAR2 $VAR3 $VAR4 $SHELL /bin/bash
注意: 在这些操作开始时,Bash shell 的 PID 是 30576。
第二个 Bash shell 的 PID 是 16353,其父 shell 的 PID 是 30576,也就是原来的 Bash shell。
我们在第二个 Bash shell 中创建了 VAR1、VAR2 和 VAR3 三个变量,但是只导出了 VAR2 和 VAR3。
在 Korn shell 中,我们创建了 VAR4。echo 命令只显示了 VAR2、VAR3 和 VAR4 的值,这就证实了 VAR1 的确没有导出。看到提示符改变之后,SHELL 变量的值却还未改变,您会非常奇怪么?通常不能总依赖 SHELL 来告诉您正在哪个 shell 下运行,不过 ps 命令的确可以告诉您实际的命令。注意 ps 会在第一个 Bash shell 前面放上一个连字符(-)来说明这是一个登录 shell。
现在回到第二个 Bash shell 中,我们可以看到 VAR1、VAR2 和 VAR3。
最后,当我们返回到原始的 shell 中时,新变量都不存在了。
清单 2 显示了在这些常用的 bash 变量中可以看到什么。
[ian@echidna ian]$ echo $USER $UID ian 500 [ian@echidna ian]$ echo $SHELL $HOME $PWD /bin/bash /home/ian /home/ian [ian@echidna ian]$ (exit 0);echo $?;(exit 4);echo $? 0 4 [ian@echidna ian]$ echo $$ $PPID 30576 30575
在诸如 C 和 tcsh shell 之类的 shell 中,可以使用 set 命令在 shell 中设置变量,使用 setenv 命令来设置并导出变量。清单 3 中给出的语法与 export 命令的语法稍有不同。请注意在使用 set 命令时使用的等号(=)。
ian@attic4:~$ echo $VAR1 $VAR2 ian@attic4:~$ csh % set VAR1=var1 % setenv VAR2 var2 % echo $VAR1 $VAR2 var1 var2 % bash ian@attic4:~$ echo $VAR1 $VAR2 var2




回页首
可以使用 unset 命令从 Bash shell 中清除变量。可以使用 -v 选项来确保删除变量定义。函数可以使用与变量相同的名字,因此如果希望清除函数定义,就请使用 -f 选项。在没有使用 -f 或 -v 的情况下,如果存在这样一个变量,那么 bash 的 unset 命令就会清除变量定义;否则,如果存在这样一个函数,这个命令就清除函数定义(函数将在后面的Shell 函数 一节中更详细地加以介绍)。
ian@attic4:~$ VAR1=var1 ian@attic4:~$ VAR2=var2 ian@attic4:~$ echo $VAR1 $VAR2 var1 var2 ian@attic4:~$ unset VAR1 ian@attic4:~$ echo $VAR1 $VAR2 var2 ian@attic4:~$ unset -v VAR2 ian@attic4:~$ echo $VAR1 $VAR2
默认情况下,bash 会将取消的变量视为该变量的值为空,因此您可能会纳闷为什么一定要取消变量,为什么不仅仅为其赋一个空值呢。如果引用了未定义的变量,Bash 和很多其他 shell 都会允许您生成一个错误。使用命令 set -u 可以针对引用未定义的变量的情况生成一个错误,使用 set +u 可以禁用这种警告,如清单 5 所示。
ian@attic4:~$ set -u ian@attic4:~$ VAR1=var1 ian@attic4:~$ echo $VAR1 var1 ian@attic4:~$ unset VAR1 ian@attic4:~$ echo $VAR1 -bash: VAR1: unbound variable ian@attic4:~$ VAR1= ian@attic4:~$ echo $VAR1 ian@attic4:~$ unset VAR1 ian@attic4:~$ echo $VAR1 -bash: VAR1: unbound variable ian@attic4:~$ unset -v VAR1 ian@attic4:~$ set +u ian@attic4:~$ echo $VAR1 ian@attic4:~$
注意取消一个不存在的变量并不会产生错误,即使在指定 set -u 时也是如此。




回页首
在登录 Linux 系统时,您的 id 就有了一个默认 shell,它就是您的登录 shell。如果这个 shell 是 bash,那么它就会在您控制系统之前先执行几个配置脚本。如果存在 /etc/profile 文件,就首先执行这个文件。根据发行版的不同,/etc 中的其他脚本也可能会执行,例如 /etc/bash.bashrc 或 /etc/bashrc。这些脚本运行之后,如果主目录中存在脚本,该脚本也会被执行。Bash 会按照 ~/.bash_profile、~/.bash_login 和 ~/.profile 的顺序来查找文件。最先找到的文件会首先执行。
当您登出系统时,如果主目录中存在 ~/.bash_logout 脚本,bash 就会执行它。
一旦登录进系统并使用 bash,您还可以启动另外一个 shell(称为交互式 shell)来运行命令,例如在后台运行命令。在这种情况中,bash 只会执行 ~/.bashrc 脚本(假设这个脚本存在)。通常可以使用如清单 6 所示的命令在 ~/.bash_profile 检查这个脚本,以便可以在登录时或在启动交互式 shell 时执行它。
# include .bashrc if it exists if [ -f ~/.bashrc ]; then . ~/.bashrc fi
可以使用 --login 选项强制 bash 像登录 shell 一样读取配置文件。如果不希望执行登录 shell 的配置文件,可以指定 --noprofile 选项。类似地,如果希望对某个交互式 shell 不执行 ~/.bashrc 文件,可以使用 --norc 选项来启动 bash。也可以通过指定 --rcfile 选项加上希望使用的文件名来强制 bash 使用 ~/.bashrc 之外的文件。清单 7 展示了创建一个名为 testrc 的简单文件并使用 --rcfile 选项来使用这个文件的例子。注意 VAR1 变量并不是 在外部 shell 中设置的,而是通过 testrc 文件针对内部 shell 设置的。
ian@attic4:~$ echo VAR1=var1>testrc ian@attic4:~$ echo $VAR1 ian@attic4:~$ bash --rcfile testrc ian@attic4:~$ echo $VAR1 var1
除了前面介绍的这种在终端中运行 bash 的标准方法之外,bash 也可以通过其他方法加以使用。
除非您引用(source) 脚本在当前 shell 中运行,否则它就会在自己的非交互式 shell 中运行,上面的配置文件都不会被读取。然而,如果设置了 BASH_ENV 变量,那么 bash 就会对这个值进行扩展,并假设它是一个文件名。如果这个文件存在,那么 bash 就会在非交互式 shell 中执行任何脚本或命令之前先执行这个文件。清单 8 通过两个简单的文件展示了这一点。
ian@attic4:~$ cat testenv.sh #!/bin/bash echo "Testing the environment" ian@attic4:~$ cat somescript.sh #!/bin/bash echo "Doing nothing" ian@attic4:~$ export BASH_ENV="~/testenv.sh" ian@attic4:~$ ./somescript.sh Testing the environment Doing nothing
非交互式 shell 也可以使用 --login 选项启动,从而强制配置文件的执行。
Bash 也可以使用 --posix 选项以 POSIX 模式启动。这种模式与非交互式 shell 非常类似,只不过在这种模式下,要执行的文件是在 ENV 环境变量中设定的。
在 Linux 系统中常常会使用一个符号链接来以 /bin/sh 运行 bash。当 bash 检测到它正在以 sh 的名义运行时,它就会试图遵循老式 Bourne shell 的启动行为,而同时又可以兼容 POSIX 标准。当作为登录 shell 运行时,bash 会试图读取并执行 /etc/profile 和 ~/.profile 文件。当使用 sh 命令作为一个交互式 shell 运行时,bash 会试图执行由 ENV 变量指定的文件,与在 POSIX 模式下被调用时一样。当作为 sh 交互运行时,它只 会使用由 ENC 变量指定的文件;--rcfile 选项会一直被忽略。
如果 bash 是由远程 shell 守护进程调用的,那么它的行为就与交互式 shell 非常类似,如果存在 ~/.bashrc 文件就会使用该文件。




回页首
Bash shell 允许为命令定义一些 别名。使用别名的最常见原因是为了给命令提供其他名字,或者为命令提供一些默认参数。很多年以来,vi 编辑器一直都是 UNIX 和 Linux 系统上的一个主要工具。vim(Vi IMproved)编辑器与 vi 非常类似,不过有很多改进。因此如果您在使用编辑器时习惯于输入 “vi”,但是实际上却更喜欢使用 vim,那么您就可以借助于别名。清单 9 显示了如何使用 alias 命令来实现这种功能。
[ian@pinguino ~]$ alias vi=‘vim‘ [ian@pinguino ~]$ which vi alias vi=‘vim‘ /usr/bin/vim [ian@pinguino ~]$ /usr/bin/which vi /bin/vi
注意在这个例子中,如果使用 which 命令来查看 vi 程序的位置,那就会看到两行输出:第一个是别名,第二个是 vim 的位置(/usr/bin/vim)。然而,如果使用完整路径来执行 which 命令(/usr/bin/which),就可以获得 vi 命令的位置。如果您猜测这可能意味着 which 命令本身在这个系统上就是一个别名,那么您就猜对了。
可以使用 alias 命令来显示所有的别名(如果没使用任何选项,或者只使用了 -p 选项),还可以通过给出别名作为参数但不进行赋值来显示一个或多个别名。清单 10 显示了 which 和 vi 的别名。
[ian@pinguino ~]$ alias which vi alias which=‘alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde‘ alias vi=‘vim‘
which 命令的别名有些奇怪。为什么会将 alias 命令(没有参数)的输出定向到 /usr/bin/which 上呢?如果查看一下 which 命令的手册页,就会发现 --read-alias 选项通知 which 从标准输入读取一个别名列表,并将匹配项输出到标准输出设备上。这允许 which 命令报告别名和 PATH 中的命令,这种用法非常常见,因此您的发行版可能已将其作为默认设置了。这是很好的一个做法,因为如果别名和命令名相同,那么 shell 就首先执行别名。知道了这一点以后,就可以使用 alias which 来加以检查。还可以通过运行 which which 命令来了解是否为 which 命令设置了这种别名。 .
别名的另外一种常见用法是自动为命令添加参数,正如在上面看到的 which 命令的 --read-alias 和其他几个参数一样。这种方法也可用在 root 用户使用 cp、mv 和 rm 命令的时候,这样在删除或覆盖文件之前能够显示一个提示。具体用法如清单 11 所示。
[root@pinguino ~]# alias cp mv rm alias cp=‘cp -i‘ alias mv=‘mv -i‘ alias rm=‘rm -i‘




回页首
在之前的教程 “LPI 101 考试准备(主题 103):GNU 和 UNIX 命令” 中,您已经学习了命令序列 或列表。您刚刚又看到了别名中使用的管道(|)操作符,您也可以使用命令列表。举个简单的例子来说,假设您希望使用一个命令来显示当前目录中的内容,以及当前目录及其子目录所使用的空间。让我们就将其称为 lsdu 命令。因此您可以简单地将 ls 和 du 命令序列赋值给别名 lsdu。清单 12 给出了实现这种功能的正确方法和错误方法。在阅读之前请仔细查看一下,并考虑为什么第一次尝试会失败。
[ian@pinguino developerworks]$ alias lsdu=ls;du -sh # Wrong way 2.9M . [ian@pinguino developerworks]$ lsdu a tutorial new-article.sh new-tutorial.sh readme tools xsl my-article new-article.vbs new-tutorial.vbs schema web [ian@pinguino developerworks]$ alias ‘lsdu=ls;du -sh‘ # Right way way [ian@pinguino developerworks]$ lsdu a tutorial new-article.sh new-tutorial.sh readme tools xsl my-article new-article.vbs new-tutorial.vbs schema web 2.9M .
在引用构成别名的完整序列时需要非常仔细。如果使用 shell 变量作为别名的一部分,还需要注意是使用双引号还是使用单引号。您希望在定义或执行别名时让 shell 对变量进行扩展吗?清单 13 显示了创建名为 mywd 定制命令来打印当前工作目录名的错误方法。
[ian@pinguino developerworks]$ alias mywd="echo \"My working directory is $PWD\"" [ian@pinguino developerworks]$ mywd My working directory is /home/ian/developerworks [ian@pinguino developerworks]$ cd .. [ian@pinguino ~]$ mywd My working directory is /home/ian/developerworks
注意双引号会导致 bash 在执行命令之前就对变量进行扩展。清单 14 使用了 alias 命令来显示所生成的别名实际上是什么样子,从中可以看出我们的错误是很明显的。清单 14 还给出了定义这个别名的正确方法。
[ian@pinguino developerworks]$ alias mywd alias mywd=‘echo \"My working directory is $PWD\"‘ [ian@pinguino developerworks]$ mywd "My working directory is /home/ian/developerworks" [ian@pinguino developerworks]$ cd .. [ian@pinguino ~]$ mywd "My working directory is /home/ian"
终于成功了。




回页首
别名让您可以对某个命令或命令列表选用一种简写或其他名字。此外,还可以添加其他一些内容,例如在 which 命令中加上希望查找的程序名。当 shell 执行用户的输入时,就会对别名进行扩展;之后输入的其他内容都会在最后一个命令或命令列表执行之前添加到该扩展。这意味着只能在命令或命令列表之后添加参数,也只能在最后一个命令中使用这些参数。函数提供了更多功能,包括对参数进行处理的能力。函数是 POSIX shell 定义的一部分,在诸如 bash、dash 和 ksh 之类的 shell 中可以使用,但在 csh 或 tcsh 中不能使用。
在接下来的几节中,将逐步构建一个复杂的命令:从很小的构建块开始,逐渐在每个步骤加以完善,并将其转换成一个函数,以供以后使用。
可以使用 ls 命令显示有关文件系统中目录和文件的各种信息。假设您喜欢使用一个命令,假定就是 ldirs, 来显示目录名,所显示的内容如清单 15 所示。
[ian@pinguino developerworks]$ ldirs *[st]* tools/*a* my dw article schema tools tools/java xsl
为了保持简单性起见,本节中的例子使用了 developerWorks author package 中的目录和文件(请参看参考资料),如果您想为 developerWorks 编写文章和教程,也可以使用它们。在这些例子中,我们使用了这个包中提供的 new-article.sh 脚本来为一篇我们称之为 “my dw article” 的文章创建一个模板。
在撰写本文时,developerWorks author package 的版本是 5.6,因此如果您使用更新的版本,可能会发现一些不同之处。或者您也可以只使用自己的文件和目录。ldirs 命令也可以处理这些内容。在 developerWorks author package 提供的工具中,可以找到其他 bash 函数的例子。
如果在 ls 命令中使用了上述别名例子所示的颜色选项,请暂时忽略 *[st]* tools/*a*,这样就可以看到类似于图 1 所示的输出结果。

在本例中,目录都是使用深蓝色显示的,不过使用在本系列教程中所学到的知识还不足以解释这个问题。不过,使用-l 选项会对如何继续处理给出一点线索:目录列表在第一个位置处有一个 “d” 字符。因此第一个步骤应该是使用 grep 对这个长列表中的内容进行一些简单的过滤,如清单 16 所示。
[ian@pinguino developerworks]$ ls -l | grep "^d" drwxrwxr-x 2 ian ian 4096 Jan 24 17:06 my dw article drwxrwxr-x 2 ian ian 4096 Jan 18 16:23 readme drwxrwxr-x 3 ian ian 4096 Jan 19 07:41 schema drwxrwxr-x 3 ian ian 4096 Jan 19 15:08 tools drwxrwxr-x 3 ian ian 4096 Jan 17 16:03 web drwxrwxr-x 3 ian ian 4096 Jan 19 10:59 xsl
可以考虑使用 awk 而不是 grep,来在一个步骤中既对列表进行过滤,又截取每行的最后一部分内容,也就是目录名,如清单 17 所示。
[ian@pinguino developerworks]$ ls -l | awk ‘/^d/ { print $NF } ‘ article readme schema tools web xsl
清单 17 中的方法有一个问题:它无法正确处理名字中有空格的那些目录名,例如 “my dw article”。就像是 Linux 和我们生活中的大部分事情一样,解决一个问题通常有很多方法,不过此处的目标是学习函数的知识,因此让我们回到使用 grep 方法上来。在本系列文章中我们学过的另外一个工具是 cut,它可以从一个文件(包括 stdin)中截取出很多域。现在让我们在回过头来看一下清单 16,在文件名之前,可以看到 8 个由空格分隔的域。在之前的命令后面加上 cut 就可以得到如清单 18 所示的输出结果。注意 -f9- 选项告诉 cut 打印第 9 个域以及之后的域的内容。
[ian@pinguino developerworks]$ ls -l | grep "^d" | cut -d" " -f9- my dw article readme schema tools web xsl
如果我们在 tools 目录而不是当前目录上执行这个命令,使用这种方法存在的一个小问题就会变得十分明显,如清单 19 所示。
[ian@pinguino developerworks]$ ls -l tools | grep "^d" | cut -d" " -f9- 11:25 java [ian@pinguino developerworks]$ ls -ld tools/[fjt]* -rw-rw-r-- 1 ian ian 4798 Jan 8 14:38 tools/figure1.gif drwxrwxr-x 2 ian ian 4096 Oct 31 11:25 tools/java -rw-rw-r-- 1 ian ian 39431 Jan 18 23:31 tools/template-dw-article-5.6.xml -rw-rw-r-- 1 ian ian 39407 Jan 18 23:32 tools/template-dw-tutorial-5.6.xml
时间戳为什么会出现呢?两个模板文件都有 5 个数字的大小,而 java 目录的大小则只有 4 个数字,因此 cut 会将多出来的空格当作另外一个域分隔符来解释。
cut 命令也可以使用字符位置而不是域来进行分割。除了计算字符个数之外,bash shell 还有很多工具可以使用,因此可以尝试使用 seq 和 printf 命令来在长目录列表上面打印一个标尺,这样就可以方便地确定在什么地方对输出行的内容进行分割了。seq 命令最多可以使用 3 个参数,这就允许您可以打印出给定值之前的所有数字,或者打印出一个值到另一个值之间的所有数字,又或者打印出从某个值开始按给定的步值到第三个数值结束的所有数字。使用 seq 可以实现的其他有趣功能(包括打印 8 进制和 16 进制数字)请参看手册页。现在,让我们使用 seq 和 printf 命令来打印一个标尺,每 10 个字符处的位置就标记一下,如清单 20 所示。
[ian@pinguino developerworks]$ printf "....+...%2.d" `seq 10 10 60`;printf "\n";ls -l ....+...10....+...20....+...30....+...40....+...50....+...60 total 88 drwxrwxr-x 2 ian ian 4096 Jan 24 17:06 my dw article -rwxr--r-- 1 ian ian 215 Sep 27 16:34 new-article.sh -rwxr--r-- 1 ian ian 1078 Sep 27 16:34 new-article.vbs -rwxr--r-- 1 ian ian 216 Sep 27 16:34 new-tutorial.sh -rwxr--r-- 1 ian ian 1079 Sep 27 16:34 new-tutorial.vbs drwxrwxr-x 2 ian ian 4096 Jan 18 16:23 readme drwxrwxr-x 3 ian ian 4096 Jan 19 07:41 schema drwxrwxr-x 3 ian ian 4096 Jan 19 15:08 tools drwxrwxr-x 3 ian ian 4096 Jan 17 16:03 web drwxrwxr-x 3 ian ian 4096 Jan 19 10:59 xsl
啊哈!现在可以使用 ls -l | grep "^d" | cut -c40- 命令来截取从位置 40 处开始的内容了。我们的第一反应是这也没有真正解决问题,因为更大的文件依然会将正确的分割位置向右移。您可以自己试验一下。
sed 是 UNIX 和 Linux 工具包中的一个功能非常强大的编辑过滤器,它使用了正则表达式。您知道我们的任务是从以 “d” 开头的每一个输出行去掉它前面的 8 个单词和之后的空格。可以使用 sed 来实现这种功能:使用模式匹配表达式 /^d/ 选择感兴趣的行,并使用替换命令 s/^d\([^ ]* *\)\(8\}// 将前 8 个单词替换为空字符串。使用 -n 选项可以只打印那些通过 p 命令指定的行,如清单 21 所示。
[ian@pinguino developerworks]$ ls -l | sed -ne ‘s/^d\([^ ]* *\)\{8\}//p‘ my dw article readme schema tools web xsl [ian@pinguino developerworks]$ ls -l tools | sed -ne ‘s/^d\([^ ]* *\)\{8\}//p‘ java
要学习更多有关 sed 的内容,请参看参考资料 一节的内容。
现在我们已经得到满足 ldirs 函数功能的复杂命令了,接下来应该学习如何将其编写成一个函数。函数由函数名加上后面的 () 构成,然后是一系列复合命令。对于现在来说,复合命令可以是任何命令或命令列表,使用一个分号结束,并使用一对花括号包括起来(且必须使用空格与其他符号分隔开来)。在后面Shell 脚本 一节中您将学到其他的复合命令。
注意:在 Bash shell 中,函数名前面可以加上单词 “function”,但这并不是 POSIX 规范的一部分,诸如 dash 之类的更简单的 shell 并不支持这种用法。在Shell 脚本 一节中,您将学习在使用了不同的 shell 时,如何确保脚本会被适当的 shell 解释。
在函数内部,可以使用表 4 中给出的 bash 特殊变量来引用参数。可以像其他 shell 变量一样在这些变量前面加上一个 $ 符号来引用这些变量。
表 4. 函数的 Shell 参数 参数 用途
0, 1, 2, ... 从参数 0 开始的位置参数。参数 0 指的是启动 bash 的程序名;如果函数是在一个 shell 脚本中运行的,就是这个 shell 脚本的名字。有关其他可能的信息,请参看 bash 的手册页,例如使用 -c 参数启动 bash 时的情况。以单引号或双引号括起来的字符串都会当作一个参数传递,引号会被剥离掉。在双引号的情况中,诸如 $HOME 之类的 shell 变量会在调用函数之前被展开。您可能需要使用单引号或双引号来传递参数,这些参数可以包含对 shell 具有特殊意义的嵌入空格或其他字符。
* 从参数 1 开始的位置参数。如果已经把双引号中的内容展开了,那么展开后就是一个单词,使用域间分隔符(IFS)特殊变量的第一个字符来分隔参数;如果 IFS 为空,就不会插入任何分隔。默认的 IFS 值可以是空白、制表符和换行符。如果 IFS 没有设置,那么所使用的分隔符就是空白,就像默认的 IFS 一样。
@ 从参数 1 开始的位置参数。如果已经把双引号中的内容展开了,那么每个参数都变成一个单词,因此 “$@” 就等于 “$1”“$2”...。如果参数中可能会包含嵌入空白,就可以使用这种格式。
# 参数个数,不包括参数 0。
注意: 如果参数多于 9 个,就不能使用 $10 来引用第 10 个参数。而必须首先处理或保存第一个参数($1),然后使用 shift 命令来删除第 1 个参数,并将其他参数下移 1 位,这样 $10 就变成了 $9,依此类推。$# 的值也同时会被更新,从而反应剩余参数的个数。
现在可以定义一个简单函数,其功能仅仅是说明有多少个参数,并显示这些参数;如清单 12 所示。
[ian@pinguino developerworks]$ testfunc () { echo "$# parameters"; echo "$@"; } [ian@pinguino developerworks]$ testfunc 0 parameters [ian@pinguino developerworks]$ testfunc a b c 3 parameters a b c [ian@pinguino developerworks]$ testfunc a "b c" 2 parameters a b c
不管使用的是 $*、"$*"、$@ 还是 "$@",在上面这个函数的输出结果中并没有太大区别,不过当问题变得复杂时,可以肯定区别将会变得非常大。
现在,用这个到目前为止最为复杂的命令来创建一个 ldirs 函数,使用 “$@” 表示参数。可以像前面的例子一样将全部函数都输入到一行中;当然 bash 也允许在多行中输入命令,在这种情况中会自动添加分号,如清单 23 所示。清单 23 还显示了使用 type 命令来显示函数定义。注意在 type 的输出结果中, ls 命令已经被它别名的展开值替换掉了。如果需要避免这个问题,可以使用 /bin/ls 而不是单单的 ls。
[ian@pinguino developerworks]$ # Enter the function on a single line [ian@pinguino developerworks]$ ldirs () { ls -l "$@"|sed -ne ‘s/^d\([^ ]* *\)\{8\}//p‘; } [ian@pinguino developerworks]$ # Enter the function on multiple lines [ian@pinguino developerworks]$ ldirs () > { > ls -l "$@"|sed -ne ‘s/^d\([^ ]* *\)\{8\}//p‘ > } [ian@pinguino developerworks]$ type ldirs ldirs is a function ldirs () { ls --color=tty -l "$@" | sed -ne ‘s/^d\([^ ]* *\)\{8\}//p‘ } [ian@pinguino developerworks]$ ldirs my dw article readme schema tools web xsl [ian@pinguino developerworks]$ ldirs tools java
现在您的函数似乎已经可以正常工作了。但是如果像清单 24 那样运行 ldirs * 会如何呢?
[ian@pinguino developerworks]$ ldirs * 5.6 java www.ibm.com 5.6
感到惊奇吗?实际上,您并没有找到当前目录中的目录,而是找到了第 2 级子目录的内容。查看一下 ls 命令的手册页或本系列前面的教程就可以理解这是为什么了。或者像清单 25 那样运行 find 命令来查找第 2 级子目录名。
[ian@pinguino developerworks]$ find . -mindepth 2 -maxdepth 2 -type d ./tools/java ./web/www.ibm.com ./xsl/5.6 ./schema/5.6
使用通配符暴露了这种方法在逻辑上存在的一个问题。我们忽略了这样的一个事实,即不使用任何参数时 ldirs 显示的是当前目录的子目录,而 ldirs tools 显示的是 tools 目录中的 java 子目录,而不是 tools 目录本身,这与将 ls 命令用于文件而非目录的情形是一样的。理想情况下,如果没有给定参数,就应该使用 ls -l;如果给定了一些参数,就应该使用 ls -ld 命令。可以使用 test 命令来测试参数个数,然后使用 && 和 || 来构建一个命令列表,并执行适当的命令。使用 test 的 [  test expression ] 格式,您的表达式可能会是这样: { [ $# -gt 0 ] &&/bin/ls -ld "$@" || /bin/ls -l } | sed -ne ...。
不过这段代码还有一个小问题,如果 ls -ld 命令不能找到任何匹配文件或目录,就会产生一条错误消息,并返回一个非 0 的退出代码,这会导致 ls -l 命令也会被执行。这可能并不是我们所期望的。一个解决的方案是为第一个 ls 命令构造一个复合命令,这样如果命令失败,就可以对参数个数再次进行测试。可以对原来的函数进行扩充来包含这种功能,现在这个函数应该如清单 26 所示。可以利用清单 26 中的参数来尝试使用该函数,也可以利用您自己的参数来体验一下,看这个函数是怎样工作的。
[ian@pinguino ~]$ type ldirs ldirs is a function ldirs () { { [ $# -gt 0 ] && { /bin/ls -ld "$@" || [ $# -gt 0 ] } || /bin/ls -l } | sed -ne ‘s/^d\([^ ]* *\)\{8\}//p‘ } [ian@pinguino developerworks]$ ldirs * my dw article readme schema tools web xsl [ian@pinguino developerworks]$ ldirs tools/* tools/java [ian@pinguino developerworks]$ ldirs *xxx* /bin/ls: *xxx*: No such file or directory [ian@pinguino developerworks]$ ldirs *a* *s* my dw article readme schema schema tools xsl
现在,在清单 26 中给出的这个例子中,可以看到一个目录被列出了两次。如果希望,可以通过 sort | uniq 对 sed 的输出结果进行过滤,从而扩充原来的函数来解决这个问题。
从一些基本的构造块开始,现在您已经构建了一个非常复杂的 shell 函数了。




回页首
您在终端会话中输入的击键组合,以及在诸如 FTP 之类的程序中使用的击键组合,都是由 readline 库进行处理的,并且可以进行配置。默认情况下,定制文件是主目录中的 .inputrc 文件;如果系统中存在这个文件,就会在 bash 启动过程中读取这个文件。可以通过设置 INPUTRC 变量来配置不同的文件。如果没有设置这个变量,就会使用主目录中的 .inputrc 文件。很多系统在 /etc/inputrc 中都有一个默认的键映射,因此您通常会希望使用 $include 指令来包含它。
清单 27 展示了如何将 ldirs 函数绑定到 Ctrl-t 的键盘组合上(按下并一直按着 Ctrl 键,然后按下 t)。如果希望此命令执行时不使用任何参数,可以在配置行末尾添加 \n。
# My custom key mappings $include /etc/inputrc
可以通过先按 Ctrl-x 再按 Ctrl-r 来强制再次读取 INPUTRC 文件。注意如果没有自己的 .inputrc 文件,有些发行版会设置 INPUTRC=/etc/inputrc,因此如果您在这种系统上创建了 .inputrc 文件,就需要先登出系统,然后再登录一次,这样才能使用新的定义。只将 INPUTRC 设置为空或将其指向新文件只会重新读取原来的文件,而不是新的规范。
INPUTRC 文件可以包括一些条件规范。例如,您的键盘行为可能会根据您使用的是 emacs 编辑模式(bash 默认值)还是 vi 模式而有所不同。有关如何定制键盘的更多细节,请参看 bash 的手册页。




回页首
您可以将自己的别名和函数添加到自己的 ~/.bashrc 文件中,不过也可以将它们保存到任何您喜欢的文件中。不管怎样做,都请记住使用 source 或 . 命令来引用这些文件,这样就会读取文件的内容,并在当前环境中执行这个文件。如果创建了一个脚本并简单执行它,那么这个脚本就是在一个子 shell 中执行的,当这个子 shell 退出并将控制权返回给您时,所有有价值的定制就全部丢失了。
在下一节中,将学习如何超越这些简单的函数,如何添加一些编程结构,例如条件测试和循环结构,并将它们与多个函数结合起来来创建或修改 bash shell 脚本。




回页首