学习bash第二版-第五章 流程控制

  如果你是一名程序员,已经读过上一章的内容(这里声明bash具有高级编程功能),你可能会问哪里介绍了它所具有的常规语言特性。也许最明显的“遗漏”是流程控制,如if、for、while等。
  流程控制给出编程者依据诸如变量取值、命令是否正确执行及其他条件,而使程序的一部分被执行或某部分被重复执行的能力。这里将介绍控制程序执行流程的功能。
  目前介绍的几乎所有的shell脚本或函数都没有流程控制——它们只是要运行的命令的列表。然而,像C shell和Bourne shell一样,bash具有流程控制能力。本章将介绍这一点,这里将使用它们来增强上一章给出的某些编程任务的解决方案并解决本章中的任务。
  尽管我们试图解释流程控制以便非程序员也可以理解,我们也考虑了那些厌烦了tabula rasa说教的程序员。为此,这里的讨论基本是那些程序员可能已经知道的bash流程控制机制。如果你对流程控制概念已经有了基本认识,就会对本章内容有更深刻的理解。
  bash支持下述流程控制结构:
  if/else
    如果某条件为真/假,执行一个语句列表
  for
    执行一个语句列表固定次数
  while
    当某条件为真时重复执行某语句列表
  until
    重复执行某语句列表直至某条件为真
  case
    依据一个变量取值执行几个语句列表中的一个
  另外,bash还给出一个新的流程控制结构:
  select
    允许用户从一个菜单的可选列表中选择一个
  下面分别加以介绍。
**if/else
  最简单的流程控制结构是嵌入在bash的if语句中的条件语句。当选择做或不做某件事情或是依据条件表达式的真或假从数量不多的几个事情里选择一个进行操作时可以使用条件语句。条件语句测试包括shell变量的值、文件字符特性、命令是否成功运行及其他因素。shell有许多内置的测试集合与shell的编程任务相关。
  if结构语法如下:
  if condition
  then
      statements
  [elif condition
      then statements...]
  [else 
      statements]
  fi
  最简形式(没有elif和else部分或子句)中只有当条件为真时才会执行语句。如果加入一个else子句,则会具有条件为真时执行一个语句集合,条件为假时执行另一语句的语句集合的功能。可以随意使用许多elif(else if的缩写)子句,它们产生更多的条件,因而执行什么语句有更多的选择。如果使用一个或多个elif,可以将else子句看作“所有的else都失败”的部分。
**退出状态和返回
  也许该语句区别于常规的C语言和Pascal语句的唯一方面是“条件”实际上是语句列表而不是一般的布尔(真或假)表达式。如何判断条件的真或假?这不得不涉及到我们还没有提过的一般的UNIX概念:命令的退出状态。
  每个UNIX命令,无论它是来自C或其他语言的源码,还是一个shell脚本/函数,当其结束时都对其调用进程(这里即为shell)返回一个整数值。这称为退出状态。0通常为无错退出状态,而其他(1~255)通常表示错误。
  if检查if关键字后的列表中最后一个语句的退出状态。该列表通常只是一个语句,如果状态为0,则条件评估为真,如果是其他,条件为假。该情况对elif语句下的每个条件也适用(如果存在)。
  因此可以使用下面的逻辑编写代码:
  if command 运行成功
  then
     正常处理
  else
     错误处理
  fi
  现在我们可以增强上一章pushd函数的功能了:
  pushd ()
  {
      dirname=$1
      DIR_STACK="$dirname ${DIR_STACK:-$PWD' '}"
      cd ${dirname:?"missing directory name."}
      echo $DIR_STACK
  }
  该函数需要一个有效目录作为参数,下面看看它如何处理错误:如果没给出参数,代码的第三行打印错误信息并退出。
  然而,如果参数给出但无效,该函数也会蒙混过关。这种情况在上一章没有指出,可能发生如下情况:cd语句执行失败,使你仍在同一目录下,这也很正常,但代码的第二行把失败的目录放到了堆栈里,并且最后一行打印的消息使你相信入栈操作成功。即使将cd放在堆栈赋值前也没用,因为如果发生了错误它也不退出该函数。
  我们需要阻止失败的目录入栈,并且要打印错误信息。具体实现如下:
  pushd ()
  {
    dirname=$1
    if cd ${dirname:?"missing directory name."}    # 如果cd成功
    then
        DIR_STACK="$dirname ${DIR_STACK:-$PWD' '}" # 该目录入栈
        echo $DIR_STACK
    else
        echo still in $PWD.                        # 否则什么都不做
    fi
  }
  cd的调用现在在一个if结构里,如果cd执行成功,则返回0,运行代码的下两行,完成pushd操作。但如果cd执行失败,则返回退出状态1,pushd将会打印消息告诉你操作不成功。
  注意,在提供失败目录检查的过程中,我们已经改变了一点pushd的功能。现在堆栈启动时总带有第一个目录的两个副本。这是因为改变到新目录后$PWD被扩展,在下一节我们将解决该问题。
  用户通常依赖内置命令和标准UNIX实用程序返回适当的退出状态,但对自己的shell脚本和函数该如何处理呢?例如,如果你编写了一个cd函数,覆盖了内置的cd命令时该如何处理呢?
  假设在.bash_profile中有下面的代码:
  cd ()
  {
      builtin cd "$@"
      echo "$OLDPWD —> $PWD"
  }
  函数cd只改变目录并打印信息告诉你以前及当前的位置。因为在命令查找的shell次序上函数比大多数内置命令优先级要高,我们需要确保调用内置的cd命令,否则shell将进入一个调用该函数的无限循环,称为无限递归。
  builtin命令可以使你完成该功能。builtin告诉shell实用内置命令并忽略该名字的任何函数。使用builtin很简单:给出要执行的内置命令的名字及要传递的参数,如果你给出的不是内置命令,builtin将显示相应信息。例如,builtin: alice: not a shell builtin。
  我们要使该函数返回与内置的cd命令返回的一样的退出状态。问题是退出状态将被每个命令重置,因此如果不将其立即保存,它就会“消失”。该函数内,当echo语句运行时,内置cd命令的退出状态将消失(并设置其自身的退出状态)。
  因此,需要保存cd设置的退出状态并使用它作为整个函数的退出状态。我们没有介绍的两个shell特性可完成此功能。第一个是特殊的shell变量?,其值($?)为运行的最后一个命令的退出状态。例如:
  cd baddir
  echo $?
  使shell打印1,而下述命令使得shell打印0:
  cd gooddir
  echo $?
  因此,要保存退出状态,需要在cd命令执行后使用行es=$?将?的值设置为一个变量。
**返回
  需要的第二个特性是语句return N,它使得包含它的函数以状态N退出。N实际上可选:其默认为最后一个命令的退出状态。没有以return语句结束的函数(例如,上面见过的几个)将返回最后一个语句返回的状态。return只能用在函数以及使用source执行的shell脚本内。相比较,语句exit N将退出整个脚本,而不管其嵌套在函数内多少层。
  回到我们的例子:如果在cd函数的最后调用内置cd,则刚刚好。不过,我们需要在最后的位置加入赋值语句。因此需要保存cd的退出状态并将其返回为函数的退出状态。方式如下:
  cd ()
  {
      builtin cd "$@"
      es=$?
      echo "$OLDPWD —> $PWD"
      return $es
  }
  第2行将cd的退出状态保存在变量es内。第4行将其返回为函数的退出状态。在第七章将介绍一个基本的cd“包装程序”。
  退出状态对与函数自身功能无关的操作不是很有用。特别是你可能想使用它作为函数的“返回值”,就像在C或Pascal函数内一样。这样行不通,应该使用变量或命令替换来模拟这种效果。
**组合退出状态
  bash语法的另一神奇之处是允许你逻辑上组合退出状态,这样就可以一次测试多个内容。
  语法statement1 && statement2意思是“执行statement1,如果其退出状态为0,则执行statement2”。语法statement1 || statement2刚好相反:“执行statement1,如果其退出状态非0,则执行statement2”。首先,它们看起来像“if/then”和“if not/then”结构。但实际上它们是用在if结构的条件部分内部的——这一点C程序员可以理解。
  将此结构看作“and”和“or”可能更好。考虑如下代码:
  if statement1 && statement2
  then
      ...
  fi
  这里,先执行statement1,如果返回状态为0,则假定其运行正常,然后执行statement2。如果statement2返回状态为0,则执行then子句。相反,如果statement1失败(返回非0状态值),则statement2不执行;运行的最后一个语句实际上是statement1,它以失败告终——因此then子句不执行。综合考虑,可以判断出如果statement1和statement2都成功执行,则then语句执行。
  类似的,考虑如下代码:
  if statement1 || statement2
  then
      ...
  fi
  如果statement1成功,statement2不执行,这使得statement1成为最后一条语句,也意味着执行then子句。从另一方面说,如果statement1失败,则运行statement2,then是否运行主要依赖于statement2的成功。结果是如果statement1或statement2有一个成功的话则运行then子句。
  bash也允许你使用!对一个语句的返回状态取反操作,即逻辑“not”。在语句前加!使其失败时返回状态0,成功则返回状态1。本章最后有个例子说明这一点。
  作为测试退出状态的简单例子,假定我们需要编写一个脚本在一个文件中检查两个单词,并打印信息指出是否至少有一个单词在文件中。可以使用grep实现此功能:如果在其输出中发现给定字符串,则该脚本返回退出状态0,否则返回非0:
  filename=$1
  word1=$2
  word2=$3
   
  if grep $word1 $filename || grep $word2 $filename
  then
      echo "$word1 or $word2 is in $filename."
  fi
  如果两条grep语句有一条成功,则执行该代码的then子句。现在假定要知道输入文件中是否同时包含两个单词,方式如下:
  filename=$1
  word1=$2
  word2=$3
   
  if grep $word1 $filename && grep $word2 $filename
  then
      echo "$word1 and $word2 are both in $filename."
  fi
  本章后面会介绍更多逻辑操作符的例子。
**条件测试
  退出状态是if结构可以测试的唯一内容,但这并不意味着你只能检查命令是否运行正常。shell使用[...]结构提供了测试各种条件的方式。
  可以使用该结构检查一个文件的各种属性(它是否存在、文件类型、权限和所有者等),或比较两个文件哪个更新,以及对字符串进行比较。
  [ condition ]实际上与其他语句一样,唯一不同的是它返回一个退出状态,给出condition是否为真的信息(“[”符号后“]”符号前的空格是必须的)。因而,它完全适合if结构语法。
**字符串比较
  方括号将包含各种类型操作符的表达式括起来。下面从表5-1列出的字符串比较操作符开始(注意,没有“大于等于”或“小于等于”比较操作符)。在该表中,str1和str2表示取值为字符串的表达式。
  
  表5-1    字符串比较操作符
  操作符        如果为真
  str1 = str2   str1匹配str2
  str1 != str2  str1不匹配str2
  str1 < str2   str1小于str2
  str1 > str2   str1大于str2
  -n str1       str1为非null(长度大于0)
  -z str1       str1为null(长度为0)
  
  可以使用这些操作符增强popd函数的功能。如果试图进行在堆栈为空时弹出操作,该函数工作不正常。记得popd的代码为:
  popd ()
  {
      DIR_STACK=${DIR_STACK#* }
      cd ${DIR_STACK%% *}
      echo "$PWD"
  }
  如果堆栈为空,则$DIR_STACK为null字符串,表达式${DIR_STACK%% }亦如此。这意味着将改变到用户的主目录;我们要做的是要popd打印一个错误信息,且不做任何动作。
  实现此功能需要测试空堆栈的情况,亦即$DIR_STACK是否为null。实现方式如下:
  popd ()
  {
      if [ -n "$DIR_STACK" ]; then
          DIR_STACK=${DIR_STACK#* }
          cd ${DIR_STACK%% *}
          echo "$PWD"
      else
          echo "stack empty, still in $PWD."
      fi
  }
  在条件表达式里,将$DIR_STACK置于双引号内,这样当其被扩展时就会被当做单个单词。如果不这样做,shell会将$DIR_STACK扩展为多个分隔单词,测试将表示给出了太多的参数。
  将$DIR_STACK置于双引号内的另一原因以后会变得很重要:有时被测试的变量将扩展为空,在此例中,测试为[ -n ],它返回true。将变量置于双引号内将确保即使其扩展为空,也会被当做一个空字符串参数(即[ -n "" ])。
  还要注意,这里没有把then放在单独一行上,而是放在if行的分号后面。分号是shell的标准语句分隔字符。
  我们也可以使用与-n不同的操作符。例如,可以使用-z,并交换在then和else子句中控制代码。
  在整理我们上一章编写的代码的同时,让我们解决highest脚本(任务4-1)里的错误处理问题。该脚本代码如下:
  filename=${1:?"filename missing."}
  howmany=${2:-10}
  sort -nr $filename | head -$howmany
  如果你省略了第一个参数(文件名),shell会打印信息highest: 1: filename missing。可以通过将其替换为更一般的“可用”信息加以改进。同时,我们可以使该命令更加符合常规UNIX命令,方式是在可选参数前加斜线。
  if [ -z "$1" ]; then
      echo 'usage: highest filename [-N]'
  else
    filename=$1
    howmany=${2:--10}
    sort -nr $filename | head $howmany
  fi
  注意,我们把$howmany前面的短划线移到了参数表达式${2:--10}内。
  将所有的代码都放在if-then-else里是很好的编程习惯,但如果你正在编写一个很长的脚本,需要对错误进行检测,并可能在某些点上跳出,则这种方式可能会引起混乱。因此,可以使用一种更常用的shell编程风格。
  if [ -z "$1" ]; then
      echo 'usage: highest filename [-N]'
      exit 1
  fi
   
  filename=$1
  howmany=${2:--10}
  sort -nr $filename | head $howmany
  exit语句告知任何调用程序它是否成功运行。
  作为=操作符的例子,我们在任务4-2中的图形使用程序中加入该操作符。记得我们给定了以.pcx结尾的文件名(最初的图形文件)。需要构建同名的文件,但要以.gif结尾(输出文件)。如果能把其他类型的文件转换成gif文件,我就可以将其用于Web页面了。除了PCX以外,可能要转换的常用类型有XPM(XPixMap)、TGA(Targa)、TIFF(Tagged Image File Format)和JPEG(Joint Photographics Expert Group)。
  这里不会执行一种图形格式到另一种图形格式的实际转换操作,我们可以使用Internet上一些可以免费利用的工具,如NetPBM或Independent JPEG Group转换工具。
  不要担心这些工具的工作细节,这里需要做的就是创建一个shell前端来处理文件名,并调用正确的转换工具。唯一需要知道的就是每个转换工具都需要一个文件名作为参数并把转换结果发送到标准输出。为了减少和第三方或不同图形格式之间进行转换所需的转换程序数量,NetPBM给出自己的格式:Portable Anymap文件,也称为PNM,以及对这种扩展格式进行转换的实用程序。
  这里开发的前端脚本首先应依据文件扩展名选择正确的转换工具,然后将PNM文件转换为GIF:
  filename=$1
  extension=${filename##*.}
  ppmfile=${filename%.*}.ppm
  outfile=${filename%.*}.gif
  
  if [ -z $filename ]; then
      echo "procfile: No file specified"
      exit 1
  fi
  
  if [ $extension = gif ]; then
      exit 0
  elif [ $extension = tga ]; then
      tgatoppm $filename > $ppmfile
  elif [ $extension = xpm ]; then
      xpmtoppm $filename > $ppmfile
  elif [ $extension = pcx ]; then
      pcxtoppm $filename > $ppmfile
  elif [ $extension = tif ]; then
      tifftopnm $filename > $ppmfile
  elif [ $extension = jpg ]; then
      djpeg $filename > $ppmfile
  else
      echo "procfile: $filename is an unknown graphics file."
      exit 1
  fi
  
  ppmquant -quiet 256 $ppmfile | ppmtogif -quiet > $outfile
  
  rm $ppmfile
  前一章中表达式${filename%.*}删除filename的扩展名,${filename##*.}删除前面部分,保留扩展名。
  一旦选择了正确转换,该脚本运行该实用程序并把输出写入一个临时文件。倒数第二行语句接受该临时文件,执行某些操作,然后将其转换为一个GIF文件。最后删除临时文件。注意,如果最初的文件是一个GIF文件,这里就不做任何处理退出即可。
  该脚本还有些问题。本章后面会加以改善。
**文件属性检查
  另一种操作符用于在条件表达式中检查文件属性,共有大约20个,这里介绍常用的一些。另外的如粘着位(sticky bit)、套接字和文件描述符不是很常用,只有系统黑客才会有兴趣。完整列表请参阅附录二。表5-2给出这里要检验的一些。
  
  表5-2    文件属性操作符
  操作符           如果为真
  -d file          file存在并且为一个目录
  -e file          file存在
  -f file          file存在并且为一正规文件(亦即不是一个目录或其他特殊类型文件)
  -r file          对file有读权限
  -s file          文件存在且非空
  -w file          对file有写权限
  -x file          对file有可执行权限,如果为目录,则有目录搜索权限
  -O file          你是file的所有者
  -G file          file的组ID匹配你的ID(如果你在多个组中,则匹配其中一个)
  file1 -nt file2  file1比file2新
  file1 -ot file2  file1比file2旧
  
  在介绍例子前,应该知道[和]内的条件表达式也可使用逻辑操作符&&和||组合起来,就像在前面一节“组合退出状态”里见过的shell命令一样。例如:
  if [ condition ] && [ condition ]; then
  也可以使用如下逻辑操作符将条件表达式和shell命令组合起来:
  if command && [ condition ]; then
    ...
  也可以在条件表达式前加上!符号否定其“真”值。这样只有当expr为假时! expr值才为真。另外,可以通过使用圆括号用斜线转义防止shell对其进行特殊处理将之分组或使用没有介绍过的两个逻辑操作符:-a(AND)和-o(OR),进而实现条件操作符的复杂逻辑表达式。
  -a和-o操作符类似于退出状态时所用的&&和||操作符。然而,与它们不同,-a和-o只在test条件表达式内可用。
  下面使用两个文件操作符,一个逻辑操作符和一个字符串操作符来解决在pushd函数内堆栈项目重复的问题。这里不使用cd判断给出的参数是否为一有效目录,而是我们自己测试,如果目录无效则返回失败的退出状态。代码如下:
  pushd ()
  {
      dirname=$1
      if [ -n "$dirname" ] && [ \( -d "$dirname" \) -a \
              \( -x "$dirname" \) ]; then
          DIR_STACK="$dirname ${DIR_STACK:-$PWD' '}"
          cd $dirname
          echo "$DIR_STACK"
      else
          echo "still in $PWD."
      fi
  }
  只有当参数$1非null(-n),为一个目录(-d),且用户对其有修改权限(-x)时,条件表达式才为真。注意,此条件首先处理参数遗漏的情况($dirname为null);如果参数遗漏,条件语句其余部分不执行,这很重要,因为如果我们这样做:
  if [ \( -n "$dirname"\) -a  \( -d "$dirname" \) -a \
         \( -x "$dirname" \) ]; then
  如果为null第二个条件会使test报错,函数会过早退出。
  下面给出了使用文件操作符的更清晰的例子。
  
  任务5-1
  编写脚本打印与ls -l基本相同的信息,但方式更加友好。
  
  虽然该任务需要相对较长的编码,但也是许多文件操作符的较为直接的应用:
  if [ ! -e "$1" ]; then
      echo "file $1 does not exist."
      exit 1
  fi
  if [ -d "$1" ]; then
      echo -n "$1 is a directory that you may "
      if [ ! -x "$1" ]; then
          echo -n "not "
      fi
      echo "search."
  elif [ -f "$1" ]; then
      echo "$1 is a regular file."
  else
      echo "$1 is a special type of file."
  fi
  if [ -O "$1" ]; then
      echo 'you own the file.'
  else
      echo 'you do not own the file.'
  fi
  if [ -r "$1" ]; then
      echo 'you have read permission on the file.'
  fi
  if [ -w "$1" ]; then
      echo 'you have write permission on the file.'
  fi
  if [ -x "$1" -a ! -d "$1" ]; then
      echo 'you have execute permission on the file.'
  fi
  将此脚本命名为fileinfo。其工作方式如下:
  ·第一个条件测试作为参数给定的文件是否不存在(!为“非”操作符;其两边的空格是必须的)。如果该文件不存在,脚本打印错误消息并退出,同时带有错误的退出状态。
  ·第二个条件测试文件是否为一目录。如果是,第一个echo打印部分信息;-n选项令echo在结尾不打印LINEFEED。内部条件检查你是否对目录有搜索权限,如果没有,单词“not”被加入到前面的打印消息中,然后,打印消息以“search”和LINEFEED结束。
  ·elif子句检测文件是否为一规则文件;如果是,则打印消息。
  ·else子句说明各种当前UNIX系统上的特殊文件类型,如套接字、设备、FIFO文件等。这里假定用户对其细节不感兴趣。
  ·下一条件测试文件是否为你所有(即其所有者ID是否与你的登录ID一样)。如果是,打印消息说明你拥有该文件。
  ·下面两个条件测试你是否对文件有读和写权限。
  ·最后一个条件测试你是否能执行该文件。它检测你是否对该文件有执行权限以及文件是否不是一个目录(如果文件是一个目录,执行权限则意味着目录搜索权限)。该测试中,我们没有使用圆括号将测试分组,完全依赖于操作符优先级。简单说操作符优先级就是shell处理操作符的次序。此概念与数学中的算术优先级一样。对后者,乘和除优先于加和减。这里,[ -x "$1" -a ! -d "$1" ]等价于[\( -x "$1" \) -a \( ! -d "$1" \) ]。文件测试首先被执行,然后是符号!,然后是AND和OR测试。
  作为fileinfo输出的一个例子,假定对当前目录执行ls -l,结果包含入下内容:
  -rwxr-xr-x   1 cam      users        2987 Jan 10 20:43 adventure
  -rw-r--r--   1 cam      users          30 Jan 10 21:45 alice
  -r--r--r—   1 root     root        58379 Jan 11 21:30 core
  drwxr-xr-x   2 cam      users        1024 Jan 10 21:41 dodo
  alice和core为规则文件,dodo为目录,adventure为shell脚本。键入fileinfo adventure结果如下:
  adventure is a regular file.
  you own the file.
  you have read permission on the file.
  you have write permission on the file.
  you have execute permission on the file.
  键入fileinfo alice结果如下:
  alice is a regular file.
  you own the file.
  you have read permission on the file.
  you have write permission on the file.
  键入fileinfo dodo结果如下:
  dodo is a directory that you may search.
  you own the file.
  you have read permission on the file.
  you have write permission on the file.
  键入fileinfo core结果如下:
  core is a regular file.
  you do not own the file.
  you have read permission on the file.
**整数条件
  shell还提供算术测试集合。它们与像<和>的字符字符串比较不同。后者比较字符串的词典值,而不是数字值。例如,词典比较时“6”比“57”大,就像“p”比“ox”大一样。但作为整数比较情况则刚好相反。
  表5-3中列出了整数比较操作符。
  
  表5-3    算术测试操作符
  测试    比较
  -lt     小于
  -le     小于等于
  -eq     等于
  -ge     大于等于
  -gt     大于
  -ne     不等于
  
  你会发现在下一章整数变量的上下文中经常用到这些操作符。如果要在同一条件表达式内将整数测试和其他类型测试结合起来,它们是必须的。
  然而,shell对只包含整数的条件表达式有单独的语法。这样做效率更佳。因此应该优先使用上面列出的算术测试操作符。下一章我们会介绍shell的整数条件。
**for
  我们对前面脚本最明显的改进是报告多个文件而不是一个。如-e和-d的测试只带一个参数,因此我们需要一种方式可以在命令行上对每个给定文件调用代码一次。
  实现方式——实际上就是用bash做多个事情的方式——是使用循环结构。shell循环结构中最简单和常用的是for循环。下面我们使用for来增强fileinfo的功能。
  for循环允许你重复一段代码固定次数。在循环代码执行期间(称为循环体),一个称作循环变量的特殊变量被赋予了不同的值;这样每个循环的功能就有了细微的差别。
  for循环有时(但不完全)类似于常规语言如C和Pascal中的它的副本。基本差别是shell的for循环不允许你指定循环的次数或范围,而只能指定值的固定列表。换句话说,不能使用如下的类Pascal代码执行任何操作,这段代码执行statements 10次:
  for x := 1 to 10 do
  begin
      statements...
  end
  (需要使用下面将介绍的while来构建此类循环。你还需要整数运算的能力,在第六章中将介绍该内容)。
  然而,for循环很适合处理命令行上的参数以及文件集(例如,给定目录下所有文件)。下面看一个例子。首先,给出for结构的语法:
  for name [in list]
  do
      statements that can use $name...
  done
  list为名称列表(如果in list被省略,列表默认为"$@",即命令行参数的引用列表,但我们为安全起见常给出in list),在下面任务的解决方案中,我们给出指定列表的两种简单方式。
  
  任务5-2
  任务4-4使用模式匹配和替换列出PATH里的目录,一个一行。不过,bash的旧版本没有这样特殊的模式操作符。需要编写一个更通用的shell脚本listpath,打印PATH里的目录,一个一行。另外,要打印每个目录的信息,如权限和修改时间等。
  实现的最简单方式是通过修改第四章介绍的IFS变量:
  IFS=:
   
  for dir in $PATH
  do
      ls -ld $dir
  done
  代码将IFS设置为冒号,即在PATH中使用的分隔符。然后用for循环进行遍历,设置dir为PATH中每个冒号分隔的域。ls用来打印目录名和相关信息。-l参数指定“长”格式,-d告诉ls只显示目录本身,不显示其内容。
  代码中你会看到一个ls产生的错误。例如,ls: /usr/TeX/bin: No such file or directory。它指出PATH里该目录不存在。我们可以修改listpath脚本,对PATH变量检测不存在的目录,方法是加入前面介绍的一些测试语句:
  IFS=:
  
  for dir in $PATH; do
      if [ -z "$dir" ]; then dir=.; fi
   
      if ! [ -e "$dir" ]; then
          echo "$dir doesn't exist"
      elif ! [ -d "$dir" ]; then
          echo "$dir isn't a directory"
      else
          ls -ld $dir
      fi
  done
  这次,当脚本循环时,首先检测$dir的长度是否为0(PATH里::的取值所导致),如果是,将其设置为当前目录,然后检验目录是否不存在。如果不存在,打印相应信息。否则检验文件是否为一目录,如果不是,则打印该信息。
  该代码阐述了for的简单使用情况但更常见的是使用它来遍历命令行参数列表。为此,应增强fileinfo脚本功能,使其可接受多个参数。首先,要编写一个“包装程序”代码执行循环:
  for filename in "$@" ; do
      finfo "$filename"
      echo
  done
  接着,将最初的脚本放入函数finfo内:
  finfo ()
  {
      if [ ! -e "$1" ]; then
          print "file $1 does not exist."
          return 1
      fi
      ...
  }
  完整脚本由for循环代码和上述函数组成。好的编程习惯是先编写该函数定义。
  fileinfo脚本工作方式如下:在for语句中,"$@"为所有位置参数的列表。对每个参数,循环体是运行设置了该参数的filename。换句话说,每个$filename值都作为函数的第一个参数($1)调用一次函数finfo。finfo调用后的echo调用只在每个文件的信息集合之间打印空行。
  给定包含上个例子里相同文件的目录,键入fileinfo *输出如下结果:
  adventure is a regular file.
  you own the file.
  you have read permission on the file.
  you have write permission on the file.
  you have execute permission on the file.
   
  alice is a regular file.
  you own the file.
  you have read permission on the file.
  you have write permission on the file.
   
  core is a regular file.
  you do not own the file.
  you have read permission on the file.
   
  dodo is a directory that you may search.
  you own the file.
  you have read permission on the file.
  you have write permission on the file.
  下一个编程任务讲解了for的其他主要用途。
  
  任务5-3
  可以使用ls的-R选项打印给定目录下的所有目录。不过,这对了解目录结构信息没有太多帮助,因为它将按行打印所有文件和目录。编写一个脚本实现一个递归的目录列表,并给出少量子目录结构的信息。
  
  预计的输出结果如下:
  .
        adventure
                aaiw
                        dodo
                        duchess
                        hatter
                        march_hare
                        queen
                        tarts
                biog
                ttlg
                        red_queen
                        tweedledee
                        tweedledum
        lewis.carroll
  每列表示一个目录层。一个项目右下边的项为该目录下文件和目录。文件只被列出,其右边不再有项。这个例子显示了目录adventure和文件lewis.carroll在当前目录下。目录aaiw和ttlg及文件biog在adventure下等。简单起见,这里使用TAB将列对齐并忽略从一列到相邻列的文件名的突出部分。
  我们要在目录层次间移动,为此可以使用一种编程技术——递归。递归就是从自身再引用自己。这里就是从调用自身的一段代码。例如,考虑用户主目录下脚本tracedir:
  file=$1
  echo $file
   
  if [ -d "$file" ]; then
      cd $file
      ~/tracedir $(ls)
      cd ..
  fi
  首先复制并打印第一个参数,然后测试其是否为目录。如果是,使用cd转移到该目录,使用该目录下的文件作为参数再次调用该脚本。则该脚本被递归调用。如果第一个参数是目录,则调用一个新的shell,新的脚本运行在新的目录上。旧脚本等待新脚本返回,然后旧脚本执行退回到上一目录的操作并退出,tracedir脚本的每次调用都进行上述过程。只有当第一个参数不是目录时才会停止。
  在上面列出的目录结构下运行该脚本,且给出参数adventure,结果为:
  adventure
  aaiw
  dodo
  dodo是文件,于是脚本退出。
  此脚本出现了一些新问题,但它是该任务解决方案的基础。一个主要问题是效率很差。每次脚本被调用时,都创建了一个新的shell,可以把脚本放入一个函数以增加效率,因为(如第四章中所述)函数是其启动shell的一部分。这里还需要设置TAB间隔的方式。最简单的方式是给出一个初始化脚本或函数并从中调用递归过程。下面给出了该过程:
  recls ()
  {
      singletab="\t"
   
      for tryfile in "$@"; do
          echo $tryfile
          if [ -d "$tryfile" ]; then
              thisfile=$tryfile
              recdir $(command ls $tryfile)
          fi
      done
   
      unset dir singletab tab
  }
  首先,设置变量保存echo命令使用的TAB字符(第七章中介绍了对echo命令可以使用的选项和格式化命令)。然后遍历提供给函数的每个参数,打印它。如果它是目录,则调用递归过程,使用ls给出文件列表。这里介绍一种新的命令:command。command是shell内置命令,它屏蔽函数和别名查找,在这里用于确保ls命令来自用户的命令搜索路径PATH下,而不是函数(关于command的详细内容请参见第七章)。程序结束后,清除使用过的变量。
  现在扩展前面给出的shell脚本:
  recdir ()
  {
      tab=$tab$singletab
   
      for file in "$@"; do
          echo -e $tab$file
          thisfile=$thisfile/$file
   
          if [ -d "$thisfile" ]; then
              recdir $(command ls $thisfile)
          fi
   
          thisfile=${thisfile%/*}
      done
   
      tab=${tab%\t}
  }
  每次调用它时,recdir遍历作为参数给出的文件。打印每一个文件名,然后,如果该文件为目录,再次调用它,参数设置为该目录的内容。必须要考虑的细节有两个:使用的TAB数目和递归中“当前”目录的路径名。
  每次进入目录层次中的下一层次时,都需要增加一个TAB字符。因此每次进入recdir时把TAB附加到变量tab尾部。同样,当上移目录层次,退出recdir时删除该TAB。tab开始时没有设置,因此第一次调用recdir时,tab被设置为一个TAB。如果递归进入下一层次,recdir再次被调用,在后面附加另一TAB。记住,tab是一个全局变量,因此每次进入或每次退出recdir,他都会增长或减少一个TAB。echo的-e选项使tab识别被转义的格式化字符,这里TAB字符为\t。
  在此递归过程版本中,我们没有使用cd在目录间移动。这表明用ls列出一个目录必须给出文件沿层次结构的相对路径。为此,需要跟踪当前正在检验的目录。初始化过程将变量thisfile设置为每次循环时找到的目录名。然后该变量用于递归过程,跟踪被检验文件的相对路径。在每个循环中,thisfile都把当前文件名附加在后面,循环结束时删除该文件名。
  可以考虑修改代码的功能以使提高其输出能力。下面给出一些编程课题:
  1.在当前版本中,没有办法判断biog是否为文件或目录。列表中空目录和文件没有任何差别。改变输出结果,使每个目录名在显示时后面附加符号/。
  2.修改代码使其最多向下循环8个子目录(即最大不超过屏幕最右边的位置)。提示:考虑TAB的实现过程。
  3.改变输出使其包含短划线行并在每个目录后加入空行,像这样:
  .
  |
  |-------adventure
  |       |
  |       |-------aaiw
  |       |       |
  |       |       |-------dodo
  |       |       |-------duchess
  |       |       |-------hatter
  |       |       |-------march_hare
  |       |       |-------queen
  |       |       |-------tarts
  |       |
  |       |-------biog
  ...
  提示:你至少需要另外两个变量以包含字符“|”和“-”。
**case
  我们将介绍的下一个流程控制结构是case。Pascal中的case语句和C中类似switch语句被用来测试如整数和字符的简单值。bash的case结构使你可以依据可包含通配符的模式测试字符串。与其常规语言中的副本一样,case使你以一种更精细的方式表示一系列if-then-else类型的语句。
  case的语法如下:
  case expression in
      pattern1 )
          statements ;;
      pattern2 )
          statements ;;
      ...
  esac
  任何pattern实际上都可以由管道字符(|)分隔的几个模式组成。如果expression匹配其中一个模式,其相应语句被执行。如果存在几个管道字符分隔的模式,表达式会尝试匹配其中的每一个以使相应语句被执行。模式被按次序检测,直至成功匹配。如果都匹配不上,则不执行任何动作。
  用一个例子就可以解释清楚此结构。再次观察任务4-2的解决方案以及本章前面加入的部分(即加入的图形功能)。我们编写了一些代码,以按照输入文件的后缀对其进行处理(.pcx对应pcx格式,.jpg对应JPEG格式等)。
  有两种方式改进此方案。第一,可以使用一个for循环允许一次处理多个文件。第二,可以使用case结构使代码更漂亮:
  for filename in "$@"; do
      ppmfile=${filename%.*}.ppm
   
      case $filename in
          *.gif ) exit 0 ;;
   
          *.tga ) tgatoppm $filename > $ppmfile ;;
   
          *.xpm ) xpmtoppm $filename > $ppmfile ;;
   
          *.pcx ) pcxtoppm $filename > $ppmfile ;;
   
          *.tif ) tifftopnm $filename > $ppmfile ;;
   
          *.jpg ) djpeg $filename > $ppmfile ;;
   
              * ) echo "procfile: $filename is an unknown graphics file."
                  exit 1 ;;
      esac
   
      outfile=${ppmfile%.ppm}.new.gif
   
      ppmquant -quiet 256 $ppmfile | ppmtogif -quiet > $outfile
      rm $ppmfile
   
  done
  此代码中的case结构功能与上一版本中见到的if语句一样,但却更加清晰,容易理解。
  case语句的前6个模式匹配我们希望处理的各种文件扩展。最后一个模式匹配前面语句中未曾匹配的其他情况。它是对例外情况的处理过程,类似于C中的default情况。
  和前面的版本有点轻微的不同:我们把模式匹配和替换移到了for循环里,该循环处理所有的命令行参数。每次遍历循环时都要创建一个临时文件和一个永久文件,其名字为当前命令行参数名。
  第六章我们还会回到这个例子,进一步开发脚本,讨论如何处理命令行上的短划线选项。这里再给出一个使用case的任务。
  
  任务5-4
  编写一个函数,实现Korn shell的cd old new。cd试图在当前目录的路径名中查找字符串old。如果找到,替换以new,并试图改变到结果目录。
  
  可以通过使用case语句检查参数个数以及内置的cd命令改变目录来完成该功能。
  代码如下:
  cd()
  {
      case "$#" in
          0 | 1)  builtin cd $1 ;;
          2    )  newdir=$(echo $PWD | sed -e "s:$1:$2:g")
                  case "$newdir" in
                      $PWD)   echo "bash: cd: bad substitution" >&2 ;
                          return 1 ;;
                      *   )   builtin cd "$newdir" ;;
                  esac ;;
          *    )  echo "bash: cd: wrong arg count" 1>&2 ; return 1 ;;
      esac
  }
  该任务中的case语句测试cd命令的参数个数,并和三种情况作比较。
  对0或1个参数,要求cd如内置cd一样操作。case的第一种情况实现该功能。它包含我们以前没使用过的一些功能;在0和1之间的管道标记意味着两种模式其中之一被匹配计算成功。如果参数数目是两者中的一个,则执行内置cd。
  下一情况针对两个参数,这是我们向cd加入的新功能。必须做的第一件事情是找出并将旧的字符串替换为新的字符串。使用sed在当前目录下执行此操作,s:$1:$2:g表示字符串$2对字符串$1的全局替换,结果为newdir。如果替换没有发生,则路径名未变化。接下来的几行将使用此结果。
  另一case语句在执行cd或报告错误之间进行选择,因为新目录未发生变化。如果sed不能找到old字符串,则保持路径名不变。*开头是非当前路径名(由第一种开关捕获)的处理过程。
  你可能注意到了该代码存在的一个小问题:如果old和new字符串一样,则结果会出现bash:: cd: bad substitution。此结果应使你保持在同一目录下而不出现错误信息,但因为目录路径没有改变,代码使用内部case语句的第一种情况。问题是要知道sed是否执行了替换操作。这里把此问题留给读者考虑(提示:可以使用grep检查路径名内是否有old字符串)。
  外层case语句的最后一种情况是:如果存在多于两个的参数,则打印错误信息。
**select
  目前介绍的所有流程控制结构在Bourne shell中均可用。C shell有同样的功能,但语法不同。select则只对Korn shell和bash可用。另外,在常规编程语言中也没有类似结构。
  select使你可以很容易的生成菜单。其语法很简明,但功能很强大。其语法为:
  select name [in list]
  do
      statements that can use $name...
  done
  除了关键字select以外,它的语法与for的语法是一样的。
  像for一样,你可以省略in list,其默认为“$@”,即引用的命令行参数列表。select功能如下:
  ·生成列表内每一条目的菜单,并且将之格式化,使每一选择对应一个数字。
  ·提示用户输入数字。
  ·将已选条目保存在变量name内,已选择编号保存在内置变量REPLY中。
  ·执行条目内(体)的语句。
  ·无限循环该过程(后面将介绍如何退出)。
  下面的任务对我们的pushd和popd功能加入另一功能。
  
  任务5-5
  编写函数,允许用户从当前pushd目录堆栈中的目录列表选择一个目录。被选择的目录被移至堆栈首位置,并成为当前的工作目录。
  
  最好使用select处理目录的显示和选择。首先以下列行开始:
  selectd ()
  {
      PS3='directory? '
      select selection in $DIR_STACK; do
          if [ $selection ]; then
              #对堆栈进行操作的语句...
              break
          else
              echo 'invalid selection.'
          fi
      done
  }
  如果键入DIR_STACK="/usr /home /bin",执行该函数,结果为:
  1) /usr
  2) /home
  3) /bin
  directory?
  内置shell变量PS3包含select使用的提示字符串。其默认值"#?"不是很有用。因此上面代码第一行将其设置为相关值。
  select语句构建选择列表的菜单。如果用户输入有效数字(从1到目录数),则变量selection被设置为相应值;否则被设置为null(如果用户只是键入RETURN,则shell再次打印菜单)。
  循环体中代码检查select是否为非null。如果非空,执行我们在后面将加入的语句,然后break语句退出select循环。如果selection为null,代码打印错误信息,重复菜单和提示。
  break是退出select循环的常用方式。实际上(与其C中类似语句一样)它可用于退出我们之前所介绍过的任何控制结构(除了case,在case中其功能由两个分号代替)以及后面将介绍的while和until。我们到目前还没有介绍break,是因为使用它来退出循环被认为是一种坏的编码风格。但如果使用恰当,它可使代码更易读。当用户要进行选择时,break对退出select是必须的。
  现在向代码加入遗漏的部分:
  selectd ()
  {
      PS3='directory? '
      dirstack=" $DIR_STACK "
   
      select selection in $dirstack; do
          if [ $selection ]; then
              DIR_STACK="$selection${dirstack%% $selection *}"
              DIR_STACK="$DIR_STACK ${dirstack##* $selection }"
              DIR_STACK=${DIR_STACK% }
              cd $selection
              break
          else
              echo 'invalid selection.'
          fi
      done
  }
  头两行初始化环境变量。dirstack是在开始和结尾部分添加了空格的DIR_STACK的副本。这样列表中每个目录的形式都是空格-目录-空格。当操作目录堆栈时该形式简化了代码。
  select语句和if语句与再前面一个函数版本中的一样。if内的新代码使用bash的模式匹配功能来操作目录堆栈。
  第一个语句设置DIR_STACK为selection,后接删除了从selection到列表尾的dirstack。第二个语句将selection后的目录列表加入到DIR_STACK的结尾。下一行删除在函数开始部分附加在串尾的空格。要完成操作,执行cd转移到新目录,后跟break退出select代码。
  给出一个此函数内所执行的列表操作的例子,将DIR_STACK设置为/home /bin /usr2。这种情况下,dirstack变成了/home /bin /usr2。键入selectd,结果为:
  $ selectd
  1) /home
  2) /bin
  3) /usr2
  directory?
  从列表中选择/bin后,if部分的第一个语句将DIR_STACK设置为/bin,后接删除了从/bin往后所有内容的dirstack,即/home。
  然后第二个语句接受DIR_STACK,将dirstack /bin之后的内容(即/usr2)附加在它后面。DIR_STACK的值为/bin /home /usr2。尾部的空格在下一行被删除。
**while和until
  bash提供的最后两个流程控制结构是while和until。它们是类似的,都允许代码段在某条件为真(或直到其为真)时重复运行。它们也与Pascal(while/do和repeat/until)和C(while和do/until)中的类似结构相似。
  while和until在结合下一章介绍的特性,如整数运算、变量的输入/输出和命令行处理,会非常有用。但我们也可以使用已经讲过的内容给出一个可用实例。
  while语法为:
  while condition
  do
    statements...
  done
  对于until,只需将上面的while替换为until即可。像if一样,condition实际上是要运行的语句列表。最后一个语句的退出状态被用做条件取值。你可以使用带有test的条件语句,就像对if一样。
  注意,while和until的唯一差别是处理条件的方式。在while中,只要条件为真,循环就执行。在until中,只有条件为假循环才执行。until条件在循环开始时被检查,而不是像在C和Pascal中类似结构一样在结尾处。
  结果是你可以通过简单否定条件将until转换为while。until的唯一更具意义的地方如下所示:
  until command; do
      statements...
  done
  此代码含义很清楚:“执行语句,直到命令运行正确。”这可能有点别扭。
  前面的任务可以使用while重写。
  
  任务5-6
  重新实现任务5-2,要求不使用IFS变量。
  
  可用使用while和模式匹配遍历PATH列表:
  path=$PATH:
  
  while [ $path ]; do
      ls -ld ${path%%:*}
      path=${path#*:}
  done
  第一行将PATH复制到一个暂时副本path中,并附加一冒号。正常情况下冒号只用在PATH中的目录间。在结尾加一个冒号会使代码简单一些。
  在while循环内,我们使用ls显示目录,这与任务5-2一样。然后修改path,删除第一个目录路径名和冒号(这是在脚本第一行附加冒号的原因)。while使循环继续,直至$path为空字符串""(当列出path中最后一个目录时发生)。
  下面任务是until的好实例。
  
  任务5-7
  编写脚本试图将文件复制到一个目录,如果失败,等待5秒,然后再试,直至其成功。
  
  代码如下:
  until cp $1 $2; do
      echo 'Attempt to copy failed. waiting...'
      sleep 5
  done
  这是until的简单用法。首先,使用cp命令执行复制操作。如果由于某些原因不能成功,返回一个非0退出代码并退出。我们设置until循环以便复制过程结果为非0时,脚本打印消息并等待5秒。
  如前所述,until循环可被转换成while,方式是使用!操作符:
  while ! cp $1 $2; do
      echo 'Attempt to copy failed. waiting...'
      sleep 5
  done
  以我们的观点,一般很少会用到until,因而,在本书的其余部分我们都将使用while。第七章将介绍while结构的进一步应用。
 

猜你喜欢

转载自blog.csdn.net/chenzhengfeng/article/details/81558783