ABS_Guide_Cn_1

来源:互联网 发布:郑州淘宝曹帅兵 编辑:程序博客网 时间:2024/05/20 18:45

高级Bash脚本编程指南

一本深入学习shell脚本艺术的书籍

 

版本 6.0.35

2009629

作者Mendel Cooper

 

 

该教程假设你以前没有脚本知识或者编程知识,但是如果你具备该知识的话很快就能达到中级或者高级水平。整个书中涵盖了UNIX®的特有智慧和知识。你可以把它当成自学教材、手册或者shell脚本技术的参考资料。练习题和例子中的注释能够引起读者的积极性,前提是真正学习脚本的唯一途径就是写脚本。

 

本书也适合作为教材来讲解一般的介绍编程概念。

 

第一部分:介绍

 

Shell是一种命令解释器。它不仅仅是操作系统内核和用户之间的连接器,更是功能强大的编程语言。一个shell程序,被叫做一个脚本,是一个很容易使用的工具,可以通过系统调用、工具、实用程序或者编译过的二进制连接在一起。几乎所有的UNIX指令、实用程序和工具都可以通过shell脚本调用。如果这些还不够,那么shell本身类似于testingloop循环结构的命令可以增加脚本的强力支持及灵活性。Shell脚本更加适合管理系统进程以及其他重复工作的进程,而不需要那些多余的完全成熟的结构化变成语言。

 

目录

1.  Shell Programming

2.  Starting Off With a Sha-Bang

 

第一章    shell 编程

 

对于任何想精通系统管理的人来说,掌握shell脚本知识是必不可少的,即使他们之前没有真正的写过脚本。想想Linux机器的启动过程,它是通过执行/etc/rc.d 目录下的脚本去恢复系统配置及建立相关服务的。详细的了解这些启动脚本对于分析系统是非常重要的,同时还可能需要修改它。

 

掌握脚本并不难,因为这些脚本都可以分割成小的片段去学习,并且这些小的片段又是相对独立的操作[1]Shell语法很简单也很直观,类似于把一些实用程序在命令行连接起来调用,而且只用到很少的规则。绝大多少短小的脚本第一次就可以很好的运行,即使调试一个比较长的脚本也是比较直观的。

 

20世纪70年代, BASIC语言适合在早期的微机上编写程序。10年后,Bash脚本作为Linux或者UNIX的基础知识被用在更强大的机器上。

 

Shell脚本在复杂的应用程序模型设计的简捷方法。项目开发的第一阶段,使用脚本完成部分功通常都是很有用的。使用这种方法,在使用CC++JavaPerl或者Python语言完成程序编码之前,通过对应用程序结构测试、演示就能够反映程序的主要缺陷。

 

Shell脚本遵循经典的UNIX体系,把复杂的项目拆分成简单的单元,并且组件和程序之间相互连接。序贯观点认为这种方法比较好,至少比使用新一代语言更加完美的解决问题,例如Perl语言,就是尝试所有人用之去做所有事情,但是代价就是强迫你使用这种语言思考解决问题的方法。

 

根据Herbert Mayer的理论,“一种有用的语言需要数组、指针、以及泛型结构来创建数据结构”。根据这个标准,Shell脚本就不那么“有用”,或许不能……

 

我们将开始用BashBourne-Again Shell的首字母缩写组合,也是Stephen Bourne的经典Bourne ShellBash已经成了主流UNIXshell脚本,本书绝大部分原则涵盖了其他shell,比如Korn ShellBash也包含了一些Korn Shell的特性,同时也包含了一些C Shell的变种。(注意C Shell编程由于存在内在的问题不被推荐,这在199310月已经由Tom Christiansen 在网络上公告了)

 

 

 

第二章  Sha-Bang一起出发

 

一个简单的例子,一个脚本无非是将一些系统命令列在一个文件中。最起码的用处就是,在调用特殊的有顺序的命令时,可以节省工作。

 

2-1 清除:一个清除/var/log 目录下日志文件的脚本

1 # Cleanup

2 # Run as root, of course.

3

4 cd /var/log

5 cat /dev/null > messages

6 cat /dev/null > wtmp

7 echo "Logs cleaned up."

 

 

这没有什么异常的,只不过是在控制台命令行或者终端窗口一个接一个的调用一些命令,好处是不用每次都去重新敲命令。这个脚本就是一个程序——一个工具——能够很容易修改或者定制。

 

2-2 清除:一个改进的清除脚本

1 #!/bin/bash

2 # Proper header for a Bash script.

3

4 # Cleanup, version 2

5

6 # Run as root, of course.

7 # Insert code here to print error message and exit if not root.

8

9 LOG_DIR=/var/log

10 # Variables are better than hard-coded values.

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 # The right and proper method of "exiting" from a script.

 

 

现在让我们开始看一个真正的脚本,这样我们能够做更多事情……

 

2-3 清除:一个增强的和普遍的清除脚本的版本

1 #!/bin/bash

2 # Cleanup, version 3

3

4 #  Warning:

5 #  -------

6 #  This script uses quite a number of features that will be explained

7 #+ later on.

8 #  By the time you've finished the first half of the book,

9 #+ there should be nothing mysterious about it.

10

11

12

13 LOG_DIR=/var/log

14 ROOT_UID=0     # Only users with $UID 0 have root privileges.

15 LINES=50       # Default number of lines saved.

16 E_XCD=86       # Can't change directory?

17 E_NOTROOT=87   # Non-root exit error.

18

19

20 # Run as root, of course.

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 # Test whether command-line argument is present (non-empty).

29 then

30   lines=$1

31 else

32   lines=$LINES # Default, if not specified on command-line.

33 fi

34

35

36 #  Stephane Chazelas suggests the following,

37 #+ as a better way of checking command-line arguments,

38 #+ but this is still a bit advanced for this stage of the tutorial.

39 #

40 #    E_WRONGARGS=85  # Non-numerical argument (bad argument format).

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 #* Skip ahead to "Loops" chapter to decipher all this.

49

50

51 cd $LOG_DIR

52

53 if [ `pwd` != "$LOG_DIR" ]   # or   if [ "$PWD" != "$LOG_DIR" ]

54                              # Not in /var/log?

55 then

56   echo "Can't change to $LOG_DIR."

57   exit $E_XCD

58 fi  # Doublecheck if in right directory before messing with log file.

59

60 # Far more efficient is:

61 #

62 # cd /var/log || {

63 #   echo "Cannot change to necessary directory." >&2

64 #   exit $E_XCD;

65 # }

66

67

68

69

70 tail -n $lines messages > mesg.temp # Save last section of message log file.

71 mv mesg.temp messages                # Becomes new log directory.

72

73

74 #  cat /dev/null > messages

75 #* No longer needed, as the above method is safer.

76

77 cat /dev/null > wtmp   #  ': > wtmp' and '> wtmp'   have the same effect.

78 echo "Logs cleaned up."

79

80 exit 0

81 #  A zero return value from the script upon exit indicates success

82 #+ to the shell.

因为你也不希望清除所有的系统日志,因此这个改进的脚本保留了最后的部分完整日志。你可以不断的发现新的方法完善上面的脚本,提高效力。

 

***

脚本开头的#!符号告诉系统这个文件的命令需要命令解释器来解释。#!实际上就是12字节的幻数,表示这一个特殊的标记,表明该文件类型,或者表示本例是可知性shell脚本(键入man magic 来获取详更详细的丰富的主题)。紧跟这sha-bang的是路径名,这个路径在脚本中是解释命令是否是一个shell,或者是一个编程语言,或者是实用程序。然后命令解释器从第一行(sha-bang下面的一行)开始执行脚本中的命令,执行过程中忽略注释。

1 #!/bin/sh

2 #!/bin/bash

3 #!/usr/bin/perl

4 #!/usr/bin/tcl

5 #!/bin/sed -f

6 #!/usr/awk -f

 

 

2.1 调用脚本

脚本写好后,可以通过 sh 脚本名 或者 bash 脚本名 来调用它(不推荐使用 sh <脚本名 调用,因为不能有效的读取脚本中的标准输入)更有效的方法是直接赋予脚本本身可执行权限。

 

 或者:

        chmod 555 scriptname (赋予任何人 /执行 权限)

或者:

       chmod +rx scriptname(赋予任何人 /执行 权限)

      

       chmod u+rx scriptname (赋予脚本自身读/执行 权限)

 

当脚本是可执行的,就可以通过 ./脚本名字 来测试它。若果脚本是以“sha-bang”行开始的,调用脚本需要正确的命令解释器来运行。

 

最后一步,测试和调试之后你可能想把它移到/usr/local/bin目录(当然,移动需要以root用户才可以),这样的话脚本就可以在系统中被所有用户执行,然后用户就可以通过在命令行简单键入脚本名 [回车] 来调用脚本。

 

2.2 初步练习

  

1.       系统管理员常常需要写脚本使得系统一些公共进程能够自动执行,举一些例子说明这类脚本的用途。

2.       写一个脚本调用一些程序显示出系统的日期和时间,列出所有登录过的用户,显示出系统的正常运行时间,然后脚本把这些信息保存到日志文件。

 

第二部分 基础知识

 

目录

3.       特殊字符

4.       变量和参数介绍

5.       引用

6.       退出和退出状态

7.       测试

8.       操作符相关主题

 

第三章  特殊字符

 

什么是特殊字符?如果按照字面意思是一个词转意了,然后就成了特殊字符。

 

在脚本或其他地方找特殊字符

#

   注释,以 # 符号开始的行是注释(#!是例外),不会被执行

  

1 # This line is a comment.

   注释也可以跟在命令结尾后面

1 echo "A comment will follow." # 注释.

2 #                             ^ 注意 #前面有空格

   注释也可以在行的空格后面

1 # A tab precedes this comment.

   注释也可能嵌在管道符里面

1 initial=( `cat "$startfile" | sed -e '/#/d' | tr -d '/n' |/

           2 # Delete lines containing ‘#’ 注释字符

           3         sed –e `s//.//. /g’ –e ‘s/_/_ /g’` )

4 # Excerpted from life.sh script

 

       同一行上命令不能跟在注释后面,没有办法结束注释接着开始新的命令,要开始新命令就需要另起一行。

   当然在echo语句中,引用或者转码中的“#”不能作为注释的。同样的,#号出现在特定的参数替换结构中和数字常量表达式中,也不作为注释的。

1 echo "The # 不是注释"

2 echo 'The # 不是注释'

3 echo The /# 不是注释

4 echo The  # 注释

5

6 echo ${PATH#*:}        # 参数替换,不是注释

7 echo $(( 2#101011 ))   # 基数转换,不是注释

8

9 # Thanks, S.C.


        标准的应用字符和转意字符(“ /)也可以转意“#”号,固定的匹配操作也可以用“#”号。

        命令分隔符【分号】。允许两条或者多条命令在同一行。

1 echo hello; echo there

2

3

4 if [ -x "$filename" ]; then     #  注意分号后面的空格

5 #+                    ^^

6   echo "File $filename exists."; cp $filename $filename.bak

7 else   #                        ^^

8   echo "File $filename not found."; touch $filename

9 fi; echo "File test complete."

注意“;”有时候需要被转意。

;;

    终止选择【双分号】

           1 case “$variable” in

           2   abc) echo “/$variable = abc” ;;

           3   xyz) echo “/$variable = xyz” ;;

           4 esac

 ;;&,;&

    终止选择【第4版以后的Bash

.

   .”命令【句号】,相当于来源【参考例子14-22】,这是一个bash内建函数。

   .”作为文件名的组成部分,是隐藏文件的前缀,当执行ls命令时不能显示出文件名。

[root@localhost tmp]# touch .hidden-file

[root@localhost tmp]# ls -l

total 100

-rw------- 1 root root  9248 Jul 13 14:34 grub-install.img.fV3362

-rw------- 1 root root     0 Jul 13 14:34 grub-install.log.vE3363

-rw-r--r-- 1 root root 73406 Aug 18  2008 netconfig-0.8.24-1.2.2.1.i386.rpm

[root@localhost tmp]#

 

[root@localhost tmp]# ls -al

total 136

drwxrwxrwx  4 root root  4096 Jul 14 16:35 .

drwxr-xr-x 25 root root  4096 Jul  9 11:57 ..

drwxrwxrwt  2 root root  4096 Jan  9  2002 .font-unix

-rw-------  1 root root  9248 Jul 13 14:34 grub-install.img.fV3362

-rw-------  1 root root     0 Jul 13 14:34 grub-install.log.vE3363

-rw-r--r--  1 root root     0 Jul 14 16:35 .hidden-file

drwxrwxrwt  2 root root  4096 Jan  9  2002 .ICE-unix

-rw-r--r--  1 root root 73406 Aug 18  2008 netconfig-0.8.24-1.2.2.1.i386.rpm

[root@localhost tmp]#

作为目录名,“.”表示当前目录,“..”则表示上一级目录

bash$ pwd

/home/bozo/projects

 

 bash$ cd .

 bash$ pwd

 /home/bozo/projects

 

 bash$ cd ..

 bash$ pwd

 /home/bozo/

.”经常出现在移动命令中,由于这个原因,“.”表示的是当前目录。

 

bash$ cp /home/zozo/current_work/junk/* .

junk目录下所有文件复制到$PWD目录(即当前目录)。

 

.    字符匹配。“.”作为正则表达式的一部分,匹配单个字符。

 部分引用【双引号】。“STRING”保留了STRING中大多数特殊字符,详细见第5章。

全部引用【单引号】。‘STRING’保留了STRING中的所有特殊字符,‘STRING’的引用是“STRING”的较强形式。参见第5章。

逗号操作符。逗号操作符连接着连续的算术操作,所有的都是可以计算的,只有最后一个被返回。

     1  let “t2 =  ((a=9,15/3()”  #set “a =

     2         9” and “t2 = 15 / 3”

 

逗号操作符也可以连接字符串。

       1 for file in /{,usr/}bin/*calc

       2 #        ^   查找所有以“calc”结尾的可执行文件

       3 #+           /bin 目录和 /usr/bin 目录

       4 do

       5       if [ -x “$file” ]

       6       then

       7         echo $file

       8       fi

       9 done

      10      下面是查找结果

      11 # /bin/ipcalc

      12 # /usr/bin/kcalc

      13 # /usr/bin/oidcalc

      14 # /usr/bin/oocalc

      15

      16

      17 # Thank you, Rory Winston, for pointing this out.

 

    参数替代小写字母(第4bash中增加)。

/

    换码【反斜杠】。单字符引用

    /x 引用x 字符,它引用x 等同于‘X’。“/”常常被“ 和‘ 引用,所以这些表达简洁。

     参考第5章,更详细的说明反斜杠。

/

    文件名路径分隔符【向前斜线】。分隔开文件名名字中的各级路径(如/home/bozo/projects/Makefile)。

    这也是算数运算中的除法操作。

`

    命令替换`command` 结构把命令正确的输出给变量。这也是通常所说的backquotes或者backticks

    空命令【冒号】。这个shell命令等同于“NOP”(没有操作,一个空操作)。它通常被认为是shell内建的“真”的同义词,“:”指令是bash内部的函数,退出状态是true(即0)。

1        

2         Echo $?    #  0

死循环,例如:

      1 while :

      2 do

      3    operation-1

      4    operation-2

      5   

      6    operation –n

      7 done

      8

      9 #  与下面相同

     10 #    while true

     11 #    do

     12       

     13      done

if/then 例子中占位符

      1 if condition

      2 then :   # Do nothing and branch ahead

      3 else     # or else …

      4   take-some-action

      5 fi

在预期的二元操作中提供占位符。

      1 :  ${username=`whoami `}

      2 #  ${username=`whoami `}  如果不以:开始就报错

      3 #                        除非“username”是一个命令或者内建函数

 

用替换参数计算变量中的字符串(例 9-16)。

      1 ${HOSTNAME} ${USER?} ${MAIL?}

      2 #  Prints errot message

      3 #+ if one or more essential environmental variables not set.

 

变量扩展/参数替换

 

在和重定向符号>相结合时,在不改变文件权限的情况下清空文件内容,如果文件不存在则创建文件。

      1  : > data.xxx   # “data.xxx” 文件变成空

      2

3         # 效果同  cat /dev/null > data.xxx

#  然而这不能 产生一个新进程,因为“:”是内建函数

也可以参考例 15-15

 

在和重定向操作符>>连接时,对已有的目标文件不起作用(: >> target_file),如果文件不存在则创建文件。

 

  这使用语规则的文件,不包含管道符、连接符和某些特殊的文件。

也可能用在注释行的开始,尽管这中情况不被推荐。使用#注释,不对该行剩余内容错误检测,所以注释内容可以写任何东西。然而,使用“:”情况则不同:

  1  : This is a comment that generates an error, ( if [ 4x –eq 3 ] ).

“:”在/etc/passwd中和在$PATH变量中是字段分隔符。

Bash$ echo $PATH

/usr/local/bin:/bin:/usr/bin:/usr/X11R6/bin:/usr/sbin:/usr/games

 

!

       取反(或否定)。!操作反置了适当命令的退出状态(参考例6-2),意思刚好和实验的意思相反。举个例子,它能够将等于(=)变成不等于(!=),!是Bash脚本的一个关键字。

 

       在不同的环境中,!还用在变量引用中。

 

       在命令行中,!还能够调用历史记录,注意,在脚本中历史记录是隐藏的。

*

       通用字符【星号】*作为通配符用在文件名的替换中,替换给定目录的所有文件明。

Bash$ echo *

 abs-book.sgml add-drive.sh agram.sh alias.sh

 

       在正则表达式中,*可以替代任何数字字符(或者0)。

*

       算数运算。在算数运算中,*表示乘法运算。

      

       双星代表幂运算或者在文件名替换中代表补充的文件名。

?

       假设操作。在某些表达式中,?代表假设条件。

       在双括号结构中,?可以作为C语言风格中的三元操作元素,如 ?:

1         (( vr0 = var1<98?9:21 ))

2         #            ^  ^

3           

4         # if [ “$var1” –lt 98 ]

5         # then

6         #  var0=9

7         # else

8         #  var0=21

9         # fi

       参数替换中,?表示假设变量是否是设定的。

       通配符。在文件名替换中,?代表一个单个的字符,也代表正则表达式中的一个字符。

$

       变量替换。(属于变量)

1 var1=5

2 var2=23skidoo

3

4 echo $var1     # 5

5 echo $var2       # 23skidoo

       一个变量名字的$前缀表示跟着的是变量的值。

$

       行尾。在正则表达式中,文本的末尾常常有个$符号。

${}

       替换参数

$*,$@

       定位参数

$?

       退出变量$?变量保持一个命令、一个函数或者脚本本身处于退出状态。

$$

       进程序号$$变量保存了一个进程出现在脚本中的序号。

()

       命令组

1 (a=hello; echo $a)

 

       括号中的命令列表作为subshell运行。

       括号中的变量,只在内subshell中运行,对括号外的脚本程序不起作用,父进程,也就是脚本本身不能读取子进程中创建的变量,也就是在subshell中创建的变量。

1 a=123

2 ( a=321; )

3

4 echo "a = $a"   # a = 123

5 # "a" within parentheses acts like a local variable.

 

       数组初始化

1 Array=(element1 element2 element3)

 

{xxx,yyy,zzz,…}

       大括号

1 echo /"{These,words,are,quoted}/"    # " 前缀和后缀

2 # 输出的结果如 "These" "words" "are" "quoted"

3

4

5 cat {file1,file2,file3} > combined_file

6 #  file1, file2, file3合并成combined_file.

7

8 cp file22.{txt,backup}

9 # 复制"file22.txt" "file22.backup"

 

一个命令可能对大括号中以逗号分开的文件列表规格起作用[3]。文件名扩展(文件名替换)适用于大括号中的文件规格。

      大括号中不允许有空格,除非括号中的空格是引用或者转码。

      echo {file1,file2}/ :{/ A," B",' C'}

file1 : A file1 : B file1 : C file2 : A file2 : B file2 : C     #上面语句的结果

{a..z}

     大括号扩展

1 echo {a..z} #  结果是 a b c d e f g h i j k l m n o p q r s t u v w x y z

2 # 返回 a z 之间的字母

3

4 echo {0..3} # 0 1 2 3

5 #  返回 0 3 之间的数字

 

{}

     代码块【花括号】。也被当作构造函数组,事实上创建了一个匿名函数(一个没有名字的函数)。然而,与“标准”函数不同的是,代码块中的变量对于脚本来说还是可见的。

bash$ { local a;

a=123; }

bash: local: can only be used in a function

 

 

1 a=123

2 { a=321; }

3 echo "a = $a"   # a = 321   结果是代码组里面的

4

5 # Thanks, S.C.

 

 

大括号中的代码输入/输出重定向。

3-1  代码 I/O 重定向

1 #!/bin/bash

2 #   /etc/fstab中读行

3

4 File=/etc/fstab

5

6 {

7 read line1

8 read line2

9 } < $File

10

11 echo "First line in $File is:"

12 echo "$line1"

13 echo

14 echo "Second line in $File is:"

15 echo "$line2"

16

17 exit 0

18

19 # 现在考虑一下,你怎样分析单独的每一行?

20 # 提示: 可以使用 awk, 或者其他方式 . . .

21 # . . . Hans-Joerg Diers 建议使用bash内建函数 "set"

       3-2 将代码块的输出结果保存在一个文件中

1 #!/bin/bash

2 # rpm-check.sh

3

4 # 查寻一个rpm文件描述、清单

5 #+   无论这个包能不能安装

6 # 将输出结果保存在一个文件中.

7 #

8 # 这个脚本距离说明了代码块

9

10 SUCCESS=0

11 E_NOARGS=65

12

13 if [ -z "$1" ]

14 then

15 echo "Usage: `basename $0` rpm-file"

16 exit $E_NOARGS

17 fi

18

19 { # Begin code block.

20 echo

21 echo "Archive Description:"

22 rpm -qpi $1 # Query description.

23 echo

24 echo "Archive Listing:"

25 rpm -qpl $1 # Query listing.

26 echo

27 rpm -i --test $1    # Query whether rpm file can be installed.

28 if [ "$?" -eq $SUCCESS ]

29 then

30 echo "$1 can be installed."

31 else

32 echo "$1 cannot be installed."

33 fi

34 echo    # End code block.

35 } > "$1.test"   # Redirects output of everything in block to file.

36

37 echo "Results of rpm test in file $1.test"

38

39 # See rpm man page for explanation of options.

40

41 exit 0

       

      和()命令不同的是{}封装的代码常常不能创建subshell

{}

      文本占位标志{}用在xargs –i(替换字符串操作)之后表示文本占位。{}在输出文本中表示占位。

1 ls . | xargs -i -t cp ./{} $1

2 #            ^^         ^^

3

4 # From "ex42.sh" (copydir.sh) example.

 

 

原创粉丝点击