第二章:从一个Sha-Bang(#!)开始

 

Shell编程就像一个1950年代的自动点唱机…

 Larry Wall

在最简单的情况下,脚本程序不过是存储在一个文件里的系统命令列表。这至少让你执行它 时不必重新按顺序键入相同功能的命令序列。


例子 2-1. cleanup: 一个清空/var/log目录下的日志文件的脚本

   1 # Cleanup
   2 # 必须以root用户运行.
   3 
   4 cd /var/log
   5 cat /dev/null > messages
   6 cat /dev/null > wtmp
   7 echo "Logs cleaned up."

这没有什么不同寻常的,它不过是一组可以容易地从控制台或xterm(译者注:一种图形虚拟控制台). )中顺序键入的命令集。用一种脚本代替这组命令的用处是使你不必每次执行相同任务时都重复地顺序键入它们。脚本变成了一个工具, 并且它也很容易地在一个实际项目被修改或者定制。


例子 2-2. cleanup: 一个改进版的cleanup脚本

   1 #!/bin/bash
   2 # Bash脚本正确的头部.
   3 
   4 # Cleanup, 版本 2
   5 
   6 # 需要以root运行.
   7 # 如果不是root用户,在此处添加错误信息打印代码和退出代码.
   8 
   9 LOG_DIR=/var/log
  10 # 使用变量比使用硬编码(hard-coded)更好。
  11 cd $LOG_DIR
  12 
  13 cat /dev/null > messages
  14 cat /dev/null > wtmp
  15 
  16 
  17 echo "Logs cleaned up."
  18 
  19 exit # 这是从一个脚本中退出正确合适的方法

现在它看起来像一个真正的脚本了。但下面我们将做的更好…


例子 2-3. cleanup: 一个上面脚本的增强版,但不能处理错误

   1 #!/bin/bash
   2 # Cleanup, 版本 3
   3 
   4 #  注意:
   5 #  -------
   6 #  这个脚本使用了相当多的特性,这些我们稍后将会解释.
   7 #
   8 #  到那时,你已经学了这本书的一半了,你将不会再对shell感觉神秘了。
   9 #
  10 
  11 
  12 
  13 LOG_DIR=/var/log
  14 ROOT_UID=0     # 只有用户ID变量$UID值为0的用户才有root权限.
  15 LINES=50       # 默认的行数
  16 E_XCD=66       # 不能进入到目录时的退出代码值
  17 E_NOTROOT=67   # 不是root用户时退出的代码值
  18 
  19 
  20 # 必须以root用户运行,以下进行检测
  21 if [ "$UID" -ne "$ROOT_UID" ]
  22 then
  23   echo "Must be root to run this script."
  24   exit $E_NOTROOT
  25 fi  
  26 
  27 if [ -n "$1" ]
  28 # 测试是否提供了命令行参数(即是测试命令行参数至少有一个参数)
  29 then
  30   lines=$1
  31 else  
  32   lines=$LINES # Default, if not specified on command line.
  33 fi  
  34 
  35 
  36 #  Stephane Chazelas建议,
  37 #+ 下面是一种更好的检测命令行参数的方法,
  38 #+ 但是对于现在来说还是有些高级。
  39 #
  40 #    E_WRONGARGS=65  # 不是数字参数 (参数格式不对)时的退出码
  41 #
  42 #    case "$1" in
  43 #    ""      ) lines=50;;
  44 #    *[!0-9]*) echo "Usage: `basename $0` file-to-cleanup"; exit $E_WRONGARGS;;
  45 #    *       ) lines=$1;;
  46 #    esac
  47 #
  48 #* 可以跳到"循环"那章阅读开头一部分去了解上面的代码意思.
  49 
  50 
  51 cd $LOG_DIR
  52 
  53 if [ `pwd` != "$LOG_DIR" ]  # 也可以用  if [ "$PWD" != "$LOG_DIR" ]
  54                             # 如果工作目录不在/var/log里?
  55 then
  56   echo "Can't change to $LOG_DIR."
  57   exit $E_XCD
  58 fi  #在操作清空日志文件之前再次检查是否在正确的目录里
  59 
  60 # 可以像下面再次确定是否在正确的目录里:
  61 #
  62 # cd /var/log || {
  63 #   echo "Cannot change to necessary directory." >&2
  64 #   exit $E_XCD;
  65 # }
  66 
  67 
  68 
  69 
  70 tail -$lines messages > mesg.temp # 保存message日志文件最后面几行日志信息到临时文件.
  71 mv mesg.temp messages             # 然后用临时文件覆盖messages日志文件
  72 
  73 
  74 # cat /dev/null > messages
  75 #* 上面这句把messages日志文件全部清空,这样没有上面那样保留最后几行安全
  76 
  77 cat /dev/null > wtmp  #  ': > wtmp' and '> wtmp'  have the same effect.
  78 echo "Logs cleaned up."
  79 
  80 exit 0
  81 #  
  82 #一个脚本以0为退出代码表明脚本执行成功.

因为你可能并不希望把整个系统日志都清空,所以这个版本的cleanup保留了日志中最后的几行日志记录。如果你继续努力地学下去,将会发现更多精练的写法来代替上面的代码。

在脚本开头的 sha-bang ( #!) 是告诉系统这个文件是由特定命令解释器解释的一组命令。 那个 #! 实际上是两个字节的 [1] 魔数, 魔数是指定文件类型的特殊记号,在此是表示这是一个可执行的shell脚本(键入 man magic可了解更多的信息)。紧跟着#!的是一个路径名.这个路径名是解释这个脚本内命令的命令解释器程序的路径:可能是一个shell,也可能是一个编程语言或者是一个软件包程序。这个命令解释器能执行脚本内的命令语句。它从脚本开头(即从#!所在行的下一行)起执行,但是忽略注释行。 [2]

   1 #!/bin/sh
   2 #!/bin/bash
   3 #!/usr/bin/perl
   4 #!/usr/bin/tcl
   5 #!/bin/sed -f
   6 #!/usr/awk -f

上面每一个脚本头行都是不同的命令解释器,如果第一行是/bin/sh, 那就是默认的Shell(Linux系统中bash是默认的shell),否则的话就是其他的解释器. [3] 如果使用#!/bin/sh/bin/sh(因为大多不同的商业UNIX都使用Bourne shell为默认shell)可以使脚本能够移植到非Linux的机器上,虽然这样做你将不能使用Bash许多特有的属性。但这样做的脚本遵循 POSIX [4] sh标准.

值得注意的是,在"#!"后面提供的路径必须是正确的,否则你运行脚本只会收到通常像"Command not found"那样的错误信息。

如果脚本程序只是由一组普通的系统命令而没有使用Shell内置命令的话#!将被忽略。上面的第二个例子被要求以#!,开头是因为变量赋值(lines=50),这就使用了Shell的特有的语句。再次提醒使用#!/bin/sh将调用默认的命令解释器,这在Linux系统上是/bin/bash.

这份指南鼓励使用模块化的方法来写脚本。留意记录像“模板”的代码片断以备将来的脚本使用。最后你能生成一个很好的可扩展的例程库。下面的代码片断可以测试脚本是否被正确的数目参数调用。

   1 E_WRONG_ARGS=65
   2 script_parameters="-a -h -m -z"
   3 #                  -a = all, -h = help, etc.
   4 
   5 if [ $# -ne $Number_of_expected_args ]
   6 then
   7   echo "Usage: `basename $0` $script_parameters"
   8   # `basename $0`是指脚本名称(译者:这个内容在后面章节会讲).
   9   exit $E_WRONG_ARGS
  10 fi

很多时候,你会写一个执行实际功能的脚本。本章的第一个脚本就是一个例子。以后它可能会使你记起把这个脚本扩展以完成类似的任务。使用变量代替固定的字符串常量是好的办法,像这样的办法还有用函数代替反复使用的代码块。

2.1. 运行脚本

写完一个脚本,你能够运行它用命令:sh scriptname, [5] 另外也也可以用bash scriptname. 来执行(不推荐使用sh <scriptname, 因为这样会禁止脚本从标准输入里读数据)。更为方便的是你可以使用chmod命令来使脚本自身变为可执行的.

你可以:

chmod 555 scriptname (使每个人都有读和执行的权限) [6]

也可以

chmod +rx scriptname (使每个人都有读和执行的权限)

chmod u+rx scriptname (仅仅使脚本文件拥有者有读和执行的权限)

在给脚本加上执行权限之后,你可以很容易地使用./scriptname. [7] 来执行它。如果脚本以"#!"行开头,将会调用正确的命令解释器来执行它.

最后,把脚本测试并调试完后,如果想把脚本给系统中所有其他的用户使用,你应该把脚本移到目录/usr/local/bin 中(当然,这必须要有root的权限),这样只需简单地在命令行输入scriptname [回车]就能执行脚本了。

注释:

[1]

一些令人喜爱的UNIX系统(它们都基于4.2BSD)的脚本是四个字节的魔数,在字符”!”后面要求一个空格 -- #! /bin/sh.

[2]

The #!行将会命令解释器(sh或是bash)在Shell脚本中最先看到的。因为这行以#字符开头,命令解释器最终执行脚本时将会正确的把这一行当做注释行来对待。这行刚刚好只起到了它调用命令解释器的作用。

事实上,如果脚本包含了额外的#!行,bash将会把它当作一个注释.

   1 #!/bin/bash
   2 
   3 echo "Part 1 of script."
   4 a=1
   5 
   6 #!/bin/bash
   7 # 这样不会运行一个新的脚本.
   8 
   9 echo "Part 2 of script."
  10 echo $a  # Value of $a stays at 1.

[3]

利用上面所述的特性能找到一些有趣的窍门.

   1 #!/bin/rm
   2 # 删除自身的脚本.
   3 
   4 # 当你运行这个脚本时,除了这个脚本消失了之外,你不会发现更多其他的东西。
   5 
   6 WHATEVER=65
   7 
   8 echo "This line will never print (betcha!)."
   9 
  10 exit $WHATEVER  # 不要紧,脚本绝不会运行到这儿.

也可以写一个以#!/bin/more,开头的文件,并执行它。执行结果会发现这是一个自我显示的文件。 (一个使用cat命令的here document 能更好地办到这一点--参考例子 17-3).

[4]

可移植操作系统接口(Portable Operating System Interface),它尝试使类UNIX的操作系统标准化。POSIX的规范可以在站点Open Group site找到.

[5]

注意:以sh scriptname运行一个Bash脚本将会禁止所有Bash的扩展特性。因此脚本可以会因此而运行失败。

[6]

因为运行一个脚本文件需要Shell能读此文件中的命令,因此不仅需要文件的执行权限,也需要文件有读的权限。

[7]

为什么不直接用简单地键入scriptname来运行脚本? 如果当前目录(由变量$PWD)的值保存[译者注:也可由命令pwd打印当前目录路径])就是scriptname 脚本文件所在的目录,为什么直接键入scriptname不能运行脚本? 这样运行脚本可能失败。因为由于安全的因素[译者注:如果你感兴趣的话,其中一个安全漏洞可在LinuxSir的Shell版中我发的一篇帖子里找到]当前目录默认不加入到用户的$PATH.变量中。因此你必须明确地指定调用脚本的路径在当前路径./scriptname.