第二章 带着一个#!出发


在一个最简单的例子中,一个 shell 脚本其实就是将一堆系统命令列在一个文件中.它的最基本的 用处就是,在你每次输入这些特定顺序的命令时可以少敲一些字.

Example 2-1 cleanup:清除/var/log下的log文件

# Cleanup
# 当然要使用root身份来运行这个脚本

cd /var/log
cat /dev/null > messages
cat /dev/null > wtmp
echo "Logs cleaned up." 

这根本就没什么稀奇的,只不过是命令的堆积,来让从 console 或者 xterm 中一个一个的输入命令更方便一些。好处就是把所有命令都放在一个脚本中,不用每次都敲它们。这样的话,对于特定的应用来说,这个脚本就很容易被修改或定制。

Example 2-2 cleanup: 一个改良的清除脚本

#!/bin/bash
# 一个bash脚本正确的开头部分

# cleanup v2

# 当然要使用root身份来运行
# 在此处插入代码,来打印错误消息,并且在不是root身份时退出。

LOG_DIR=/var/log
# 如果使用变量,当然比把代码写死的好。
cd $LOG_DIR

cat /dev/null > messages
cat /dev/null > wtmp

echo "Logs cleaned up."

exit # 这个命令是一种正确且合适的退出脚本的方法

现在,让我们看一下一个真正意义的脚本,而且我们可以走的更远……

Example 2-3 cleanup: 一个增强的和广义的删除logfile的脚本

#!/bin/bash
# cleanup v3

# Warning:
# --------
# 这个脚本有好多特征,这些特征是在后面章节进行解释的,大概是进行到本书的一半的时候,
# 你就会觉得它没有什么神秘的了。
#
LOG_DIR=/var/log
ROOT_UID=0   # $UID为0的时候,用户才具有root用户的权限
LINES=50     # 默认的保存行数
E_XCD=66     # 不能修改目录
E_NOTROOT=67 # 非root用户将以error退出

# 当然要以root用户运行
if [ "$UID" -ne "$ROOT_UID" ]
then
  echo "Must be root to run this script." 
  exit "$E_NOTROOT"
fi

if [ -n "$1" ]
# 测试是否有命令行参数(非空)
then
  lines=$1
else
  lines=$LINES # 默认, 如果不在命令行中指定
fi

# Stephane Chazelas 建议使用下边的更好方法来检测命令行参数。
# 但对于这章来说还是有点超前。
# 
# E_WRONGARGS=65
# case "$1" in
# ""      ) lines=50;;
# *[!0-9]*) echo "Usage: `basename $0` file-to-cleanup"; exit $E_WRONGARGS;;
# *       ) lines=$1;;
# esac
#
# 直到“loops”的章节才会对上边的内容进行详细的描述

cd $LOG_DIR
if [ `pwd` != "$LOG_DIR" ] #如果当前工作路径不在$LOG_DIR中
then
  echo "Can't change to $LOG_DIR"
  exit $E_XCD
fi # 在处理log file之前,再确认一遍当前目录是否正确

# 更有效率的做法是
# cd /var/log || {
#   echo "Cannot change to necessary directory." >&2
#   exit $E_XCD
# }

tail -$lines messages > mesg.temp   # 保存log file消息的最后部分
mv mesg.temp messages  # 变为新的log目录

# cat /dev/null > messages
# 不再需要了,使用上边的方法更安全

cat /dev/null > wtmp    # ': > wtmp' 和 '> wtmp' 具有相同的作用
echo "Logs cleaned up."

exit 0 #退出前返回0,返回0表示成功。

因为你可能希望将系统 log 全部消灭,这个版本留下了 log 消息最后的部分。你将不断地找到新
的方法来完善这个脚本,并提高效率。

要注意,在每个脚本的开头都使用 #!,这意味着告诉你的系统这个文件的执行需要指定一个解释器。#! 实际上是一个 2 字节1的魔法数字,这是指定一个文件类型的特殊标记,换句话说,在这种情况下,指的就是一个可执行的脚本(键入 man magic 来获得关于这个迷人话题的更多详细信息)。在 #! 之后接着是一个路径名。这个路径名指定了一个解释脚本中命令的程序,这个程序可以是shell程序语言或者是任意一个通用程序。这个指定的程序从头开始解释并且执行脚本中的命令(从 #! 行下边的一行开始),忽略注释。2

如:

#!/bin/sh
#!/bin/bash
#!/usr/bin/perl
#!/usr/bin/tcl
#!/bin/sed -f
#!/bin/awk -f

脚本中的 #! 行的最重要的任务就是命令解释器(sh或bash)。因为这行是以# 开始的,当命令解释器执行这个脚本的时候,会把它作为一个注释行。当然,在这之前,这行语句已经完成了它的任务,那就是调用命令解释器。

如果在脚本的里边还有一个 #! 行,那么bash会将它认为是一个一般的注释行。

#!/bin/bash
echo "Part 1 of script."
a=1

#!/bin/bash
# 这将不会开始一个新脚本
echo "Part 2 of script."
echo $a #Value of $a stays at 1.

上边每一个脚本头的行都指定了一个不同的命令解释器,如果是 /bin/sh ,那么就是默认shell(在linux系统中默认是bash)。在大多数的商业发行的UNIX上,默认是Bourne shell。使用 #!/bin/sh 将让你的脚本可以正常运行在非Linux机器上,虽然这将牺牲Bash的一些独特特征。

这里可以玩一些小技巧.

#!/bin/rm
# 自删除脚本.

# 当你运行这个脚本时,基本上什么都不会发生...除非这个文件消失不见. 
WHATEVER=65

echo "This line will never print (betcha!)."

exit $WHATEVER # 没关系,脚本是不会在这退出的.

当然,你还可以试试在一个 README 文件的开头加上 #!/bin/more ,并让它具有执行权限。 结果将是文档自动列出自己的内容。(一个使用 cat 命令的 here document 可能是一个 更好的选则,--见 Example 17-3)。

注意: #! 后边给出的路径名必须是正确的,否则将会出现一个错误消息,通常是 "Command not found",这将是你运行这个脚本得到的唯一结果。

当然 #! 也可以被忽略,不过这样你的脚本文件就只能是一些命令的集合,不能使用shell内建的指令和语法了。如果不能使用变量的话,那也失去了脚本编程的意义了。

注意: 这个例子鼓励你使用模块化的方式来编写脚本,平时也要注意收集一些零碎的代码。这些零碎的代码可能用在你将来编写的脚本中。这样你就可以通过这些代码片段来构建一个较大的工程用例。 以下边脚本作为序,来测试脚本被调用的参数是否正确。

E_WRONG_ARGS=65
script_parameters="-a -h -m -z"
#  -a=all, -h=help,等等。

if [ $# -ne $Number_of_expected_args ]
then
  echo "Usage: `basename $0` $script_parameters"
  # `basename $0`是这个脚本的文件名
  exit $E_WRONG_ARGS
fi

大多数情况下,你需要编写一个脚本来执行一个特定的任务,在本章中第一个脚本就是一个这样的例子,然后你会修改它来完成一个不同的,但比较相似的任务。用变量来替代写死的常量,就是一个好方法,将重复的代码放到一个函数中,也是一个好习惯。

2.1 调用一个脚本

编写完脚本之后,你可以使用 sh scriptname2或者 bash scriptname 来调用它。
(不推荐使用 sh < scriptname,因为这禁用了脚本从stdin中读取数据的功能。)
更方便的方法是让脚本本身就具有可执行权限,通过chmod命令可以修改。
比如:
  chmod 555 scriptname (给任何人都具有可读和执行的权限) 3
或:
  chmod +rx scriptname (给任何人都具有可读和执行权限)
  chmod u+rx scriptname (只给脚本的所有者可读和执行的权限)

既然脚本已经具有了可执行权限,现在你可以使用 ./scriptname 4 来测试它了。如果这个脚本以一个 #! 行开头,那么脚本将会调用合适的脚本解释器来运行。

最后一步,在脚本被测试和debug之后,你可能想把它移动到 /usr/local/bin,来让你的脚本对所有用户都有用,这样用户就可以直接使用 scriptname 就可以使用了,像使用一个命令一样。

2.2 Exercise

  1. 系统管理员经常会为了自动化一些常用的任务而编写脚本,举出几个这种有用的脚本的实例。
  2. 编写一个脚本,显示时间和日期,列出所有的登录用户,显示系统的更新时间。然后这个脚本会把这些内容保存到一个log file中。

  1. 那些具有 UNIX 味道的脚本(基于 4.2BSD)需要一个 4 字节的魔法数字,在 #! 后边需要一个
    空格,如 #! /bin/sh

  2. 小心:使用 sh scriptname 来调用脚本的时候将会关闭一些Bash特定的扩展,脚本可能因此而调用失败。 

  3. 脚本需要读和执行的权限,因为shell需要读这个脚本。 

  4. 为什么不直接使用 scriptname 来调用脚本?如果你当前的目录下($PWD)正好有你想要执行的脚本,为什么它运行不了呢?失败的原因是,出于安全考虑,当前目录并没有被加在用户的 $PATH 变量中。因此,在当前目录下调用脚本必须使用 ./scriptname 这种 形式。