Pages

Sunday, 5 May 2013

Bash Shell编程快速入门

 

BASH 的基本语法

2.1     最简单的例子 —— Hello World!

几乎所有的讲解编程的书给读者的第一个例子都是 Hello World 程序,那么我们今天也就从这个例子出发,来逐步了解 BASH。
用 vi 编辑器编辑一个 hello 文件如下:
#!/bin/bash
# This is a very simple example
echo Hello World

这样最简单的一个 BASH 程序就编写完了。这里有几个问题需要说明一下:
一,第一行的 #! 是什么意思
二,第一行的 /bin/bash 又是什么意思
三,第二行是注释
四,echo 语句
五,如何执行该程序

#! 是说明 hello 这个文件的类型的,有点类似于 Windows 系统下用不同文件后缀来表示不同文件类型的意思(但不相同)。Linux 系统根据 “#!” 及该字串后面的信息确定该文件的类型,关于这一问题同学们回去以后可以通过 “man magic”命令 及 /usr/share/magic 文件来了解这方面的更多内容。在 BASH 中 第一行的 “#!” 及后面的 “/bin/bash” 就表明该文件是一个 BASH 程序,需要由 /bin 目录下的 bash 程序来解释执行。BASH 这个程序一般是存放在 /bin 目录下,如果你的 Linux 系统比较特别,bash 也有可能被存放在 /sbin 、/usr/local/bin 、/usr/bin 、/usr/sbin 或 /usr/local/sbin 这样的目录下;如果还找不到,你可以用 “locate bash” “find / -name bash 2> /dev/null” 或 “whereis bash” 这三个命令找出 bash 所在的位置;如果仍然找不到,那你可能需要自己动手安装一个 BASH 软件包了。
第二行的 “# This is a …” 就是 BASH 程序的注释,在 BASH 程序中从“#”号(注意:后面紧接着是“!”号的除外)开始到行尾的多有部分均被看作是程序的注释。的三行的 echo 语句的功能是把 echo 后面的字符串输出到标准输出中去。由于 echo 后跟的是 “Hello World” 这个字符串,因此 “Hello World”这个字串就被显示在控制台终端的屏幕上了。需要注意的是 BASH 中的绝大多数语句结尾处都没有分号。
如何执行该程序呢?有两种方法:

一种是显式制定 BASH 去执行:
$ bash hello 或
$ sh hello (这里 sh 是指向 bash 的一个链接,“lrwxrwxrwx 1 root root 4 Aug 20 05:41 /bin/sh -> bash”)

(如果是bash hello或sh hello这种执行方式,则无需先把hello设为755权限)

或者可以先将 hello 文件改为可以执行的文件,然后直接运行它,此时由于 hello 文件第一行的 “#! /bin/bash” 的作用,系统会自动用/bin/bash 程序去解释执行 hello 文件的:
$ chmod 755 hello
$ ./hello

此处没有直接 “$ hello”是因为当前目录不是当前用户可执行文件的默认目录,而将当前目录“.”设为默认目录是一个不安全的设置。
需要注意的是,BASH 程序被执行后,实际上 Linux 系统是另外开设了一个进程来运行的.

示例:

as3:~# nano nihao
as3:~# sh nihao
ni hao
as3:~# bash nihao
ni hao
as3:~#




 

2.2     关于输入、输出和错误输出

在字符终端环境中,标准输入/标准输出的概念很好理解。输入即 指对一个应用程序 或命令的输入,无论是从键盘输入还是从别的文件输入;输出即指应用程序或命令产生的一些信息;与 Windows 系统下不同的是,Linux 系统下还有一个标准错误输出的概念,这个概念主要是为程序调试和系统维护目的而设置的,错误输出于标准输出分开可以让一些高级的错误信息不干扰正常的输出 信息,从而方便一般用户的使用。
在 Linux 系统中:标准输入(stdin)默认为键盘输入;标准输出(stdout)默认为屏幕输出;标准错误输出(stderr)默认也是输出到屏幕(上面的 std 表示 standard)。在 BASH 中使用这些概念时一般将标准输出表示为 1,将标准错误输出表示为 2。下面我们举例来说明如何使用他们,特别是标准输出和标准错误输出。
输入、输出及标准错误输出主要用于 I/O 的重定向,就是说需要改变他们的默认设置。先看这个例子:
$ ls > ls_result
$ ls -l >> ls_result

上面这两个命令分别将 ls 命令的结果输出重定向到 ls_result 文件中和追加到 ls_result 文件中,而不是输出到屏幕上。”>”就是输出(标准输出和标准错误输出)重定向的代表符号,连续两个 “>” 符号,即 “>>” 则表示不清除原来的而追加输出。下面再来看一个稍微复杂的例子:
$ find /home -name lost* 2> err_result
这个命令在 “>” 符号之前多了一个 “2″,”2>” 表示将标准错误输出重定向。由于 /home 目录下有些目录由于权限限制不能访问,因此会产生一些标准错误输出被存放在 err_result 文件中。大家可以设想一下 find /home -name lost* 2>>err_result 命令会产生什么结果?
如果直接执行 find /home -name lost* > all_result ,其结果是只有标准输出被存入 all_result 文件中,要想让标准错误输出和标准输入一样都被存入到文件中,那该怎么办呢?看下面这个例子:
$ find /home -name lost* > all_result 2>& 1
上面这个例子中将首先将标准错误输出也重定向到标准输出中,再将标准输出重定向到 all_result 这个文件中。这样我们就可以将所有的输出都存储到文件中了。为实现上述功能,还有一种简便的写法如下:
$ find /home -name lost* >& all_result


如果那些出错信息并不重要,下面这个命令可以让你避开众多无用出错信息的干扰
$ find /home -name lost* 2> /dev/null
同学们回去后还可以再试验一下如下几种重定向方式,看看会出什么结果,为什么?
$ find /home -name lost* > all_result 1>& 2
$ find /home -name lost* 2> all_result 1>& 2
$ find /home -name lost* 2>& 1 > all_result

另外一个非常有用的重定向操作符是 “-”,请看下面这个例子:
$ (cd /source/directory && tar cf – . ) | (cd /dest/directory && tar xvfp -)
该命令表示把 /source/directory 目录下的所有文件通过压缩和解压,快速的全部移动到 /dest/directory 目录下去,这个命令在 /source/directory 和 /dest/directory 不处在同一个文件系统下时将显示出特别的优势。
下面还几种不常见的用法:
n<&- 表示将 n 号输入关闭
<&- 表示关闭标准输入(键盘)
n>&- 表示将 n 号输出关闭
>&- 表示将标准输出关闭

2.3     BASH 中对变量的规定(与 C 语言的异同)

好了下面我们进入正题,先看看 BASH 中的变量是如何定义和使用的。对于熟悉 C 语言的程序员,我们将解释 BASH 中的定义和用法与 C 语言中有何不同。

2.3.1. BASH 中的变量介绍

我们先来从整体上把握一下 BASH 中变量的用法,然后再去分析 BASH 中变量使用与 C 语言中的不同。BASH 中的变量都是不能含有保留字,不能含有 “-” 等保留字符,也不能含有空格。

2.3.1.1 简单变量

在 BASH 中变量定义是不需要的,没有 “int i” 这样的定义过程。如果想用一个变量,只要他没有在前面被定义过,就直接可以用,当然你使用该变量的第一条语句应该是对他赋初值了,如果你不赋初值也没关 系,只不过该变量是空( 注意:是 NULL,不是 0 )。不给变量赋初值虽然语法上不反对,但不是一个好的编程习惯。好了我们看看下面的例子:
首先用 vi 编辑下面这个文件 hello2
#!/bin/bash
# give the initialize value to STR
STR=”Hello World”
echo $STR

在上面这个程序中我们需要注意下面几点:

一,变量赋值时,‘=’左右两边都不能有空格;
二,BASH 中的语句结尾不需要分号(”;”);
三,除了在变量赋值和在FOR循环语句头中,BASH 中的变量使用必须在变量前加”$”符号,同学们可以将上面程序中第三行改为 “echo STR” 再试试,看看会出什么结果。==>output: STR
四,由于 BASH 程序是在一个新的进程中运行的,所以该程序中的变量定义和赋值不会改变其他进程或原始 Shell 中同名变量的值,也不会影响他们的运行。

更细致的文档甚至提到以但引号括起来的变量将不被 BASH 解释为变量,如 ‘$STR’ ,而被看成为纯粹的字符串。而且更为标准的变量引用方式是 ${STR} 这样的,$STR 自不过是对 ${STR} 的一种简化。在复杂情况下(即有可能产生歧义的地方)最好用带 {} 的表示方式。
BASH 中的变量既然不需要定义,也就没有类型一说,一个变量即可以被定义为一个字符串,也可以被再定义为整数。如果对该变量进行整数运算,他就被解释为整数;如果对他进行字符串操作,他就被看作为一个字符串。请看下面的例子:
#!/bin/bash
x=1999
let “x = $x + 1″
echo $x
x=”olympic’”$x
echo $x

关于整数变量计算,有如下几种:” + – * / % “,他们的意思和字面意思相同。整数运算一般通过 let 和 expr 这两个指令来实现,如对变量 x 加 1 可以写作:let x = $x + 1 或者 x=`expr $x + 1`
在比较操作上,整数变量和字符串变量各不相同,详见下表:
对应的操作
整数操作
字符串操作
相同
-eq
=
不同
-ne
!=
大于
-gt
>
小于
-lt
<
大于或等于
-ge

小于或等于
-le

为空

-z
不为空

-n

比如:

比较字符串 a 和 b 是否相等就写作:if [ $a = $b ]
判断字符串 a 是否为空就写作:
if [ -z $a ]
判断整数变量 a 是否大于 b 就写作:
if [ $a -gt $b ]
更细致的文档推荐在字符串比较时尽量不要使用 -n ,而用 ! -z 来代替。(其中符号 “!” 表示求反操作)
BASH 中的变量除了用于对 整数 和 字符串 进行操作以外,另一个作用是作为文件变量。BASH 是 Linux 操作系统的 Shell,因此系统的文件必然是 BASH 需要操作的重要对象,如 if [ -x /root ] 可以用于判断 /root 目录是否可以被当前用户进入。下表列出了 BASH 中用于判断文件属性的操作符:
运算符
含义( 满足下面要求时返回 TRUE )
-e file
文件 file 已经存在
-f file
文件 file 是普通文件
-s file
文件 file 大小不为零
-d file
文件 file 是一个目录
-r file
文件 file 对当前用户可以读取
-w file
文件 file 对当前用户可以写入
-x file
文件 file 对当前用户可以执行
-g file
文件 file 的 GID 标志被设置
-u file
文件 file 的 UID 标志被设置
-O file
文件 file 是属于当前用户的
-G file
文件 file 的组 ID 和当前用户相同
file1 -nt file2
文件 file1 比 file2 更新
file1 -ot file2
文件 file1 比 file2 更老

注意:上表中的 file 及 file1、file2 都是指某个文件或目录的路径

2.3.1.1. 关于局部变量

在 BASH 程序中如果一个变量被使用了,那么直到该程序的结尾,该变量都一直有效。为了使得某个变量存在于一个局部程序块中,就引入了局部变量的概念。BASH 中,在变量首次被赋初值时加上 local 关键字就可以声明一个局部变量,如下面这个例子:
#!/bin/bash
HELLO=Hello
function hello {
local HELLO=World
echo $HELLO
}
echo $HELLO
hello
echo $HELLO

该程序的执行结果是:
Hello
World
Hello

这个执行结果表明全局变量 $HELLO 的值在执行函数 hello 时并没有被改变。也就是说局部变量 $HELLO 的影响只存在于函数那个程序块中。

2.3.2. BASH 中的变量与 C 语言中变量的区别

这里我们为原来不熟悉 BASH 编程,但是非常熟悉 C 语言的程序员总结一下在 BASH 环境中使用变量需要注意的问题。
1,BASH 中的变量在引用时都需要在变量前加上 “$” 符号( 第一次赋值及在For循环的头部不用加 “$”符号 );
2,BASH 中没有浮点运算,因此也就没有浮点类型的变量可用;
3,BASH 中的整形变量的比较符号与 C 语言中完全不同,而且整形变量的算术运算也需要经过 let 或 expr 语句来处理;

2.4     BASH 中的基本流程控制语法

BASH 中几乎含有 C 语言中常用的所有控制结构,如条件分支、循环等,下面逐一介绍。

2.4.1 if…then…else

if 语句用于判断和分支,其语法规则和 C 语言的 if 非常相似。其几种基本结构为:
if [ expression ]
then
statments
fi

或者
if [ expression ]
then
statments
else
statments
fi

或者
if [ expression ]
then
statments
else if [ expression ]
then
statments
else
statments
fi

或者
if [ expression ]
then
statments
elif [ expression ]
then
statments
else
statments
fi

值得说明的是如果你将 if 和 then 简洁的写在一行里面,就必须在 then 前面加上分号,如:if [ expression ]; then … 。下面这个例子说明了如何使用 if 条件判断语句:
#!/bin/bash
if [ $1 -gt 90 ]
then
echo “Good, $1″
elif [ $1 -gt 70 ]
then
echo “OK, $1″
else
echo “Bad, $1″
fi



exit 0
上面例子中的 $1 是指命令行的第一个参数,这个会在后面的“BASH 中的特殊保留字”中讲解。

2.4.2 for

for 循环结构与 C 语言中有所不同,在 BASH 中 for 循环的基本结构是:
for $var in
    • 保留变量
    • 随机数
    • 运算符
    • 变量的特殊操作
    • BASH 中对返回值的处理
    • 用 BASH 设计简单用户界面
    • 在 BASH 中读取用户输入
    • 一些特殊的惯用法
    • BASH 程序的调试
    • 关于 BASH2
      • 中的所有项加上数字列在屏幕上等待用户选择,在用户作出选择后,变量 $var 中就包含了那个被选中的字符串,然后就可以对该变量进行需要的操作了。我们可以从下面的例子中更直观的来理解这个功能:#!/bin/bash
        OPTIONS=”Hello Quit”
        select opt in $OPTIONS; do
        if [ "$opt" = "Quit" ]; then
        echo done
        exit
        elif [ "$opt" = "Hello" ]; then
        echo Hello World
        else
        clear
        echo bad option
        fi
        done

        exit 0
        大家可以试着执行上面的程序,看看是什么执行结果。

        4.3     在 BASH 中读取用户输入

        BASH 中通过 read 函数来实现读取用户输入的功能,如下面这段程序:
        #!/bin/bash
        echo Please enter your name
        read NAME
        echo “Hi! $NAME !”

        exit 0
        上面这个脚本读取用户的输入,并回显在屏幕上。
        另外 BASH 中还提供另外一种称为 here documents 的结构????,可以将用户需要通过键盘输入的字符串改为从程序体中直接读入,如密码。下面的小程序演示了这个功能:
        #!/bin/bash
        passwd=”aka@tsinghua”
        ftp -n localhost <<FTPFTP
        user anonymous $passwd
        binary
        bye
        FTPFTP

        exit 0
        这个程序在用户需要通过键盘敲入一些字符时,通过程序内部的动作来模拟键盘输入。请注意 here documents 的基本结构为:
        command <<SOMESPECIALSTRING
        statments

        SOMESPECIALSTRING

        这里要求在需要键盘输入的命令后,直接加上 <<符号,然后跟上一个特别的字符串,在该串后按顺序输入本来应该由键盘输入的所有字符,在所有需要输入的字符都结束后,重复一遍前面 <<符号后的“特别的字符串”即表示该输入到此结束。

        4.4 一些特殊的惯用法

        在 BASH 中 () 一对括号一般被用于求取括号中表达式的值或命令的执行结果,如:(a=hello; echo $a) ,其作用相当于 `…` 。
        : 有两个含义,一是表示空语句,有点类似于 C 语言中的单个 “;” 。表示该行是一个空命令,如果被用在 while/until 的头结构中,则表示值 0,会使循环一直进行下去,如下例:
        while :
        do
        operation-1
        operation-2

        operation-n
        done

        另外 : 还可以用于求取后面变量的值,比如:
        #!/bin/bash
        : ${HOSTNAME?} {USER?} {MAIL?}
        echo $HOSTNAME
        echo $USER
        echo $MAIL

        exit 0
        在 BASH 中 export 命令用于将系统变量输出到外层的 Shell 中了。

        4.5 BASH 程序的调试

        bash -x bash-script 命令,可以查看一个出错的 BASH 脚本到底错在什么地方,可以帮助程序员找出脚本中的错误。
        另外用 trap 语句可以在 BASH 脚本出错退出时打印出一些变量的值,以供程序员检查。trap 语句必须作为继 “#!/bin/bash” 后的第一句非注释代码,一般 trap 命令被写作: trap ‘message $checkvar1 $checkvar2′ EXIT 。

        4.6 关于 BASH2

        使用 bash -version 命令可以看出当前你正在使用的 BASH 是什么版本,一般版本号为1.14或其他版本。而现在机器上一般还安装了一个版本号为 2.0 的 BASH 。该 BASH 也在 /bin 目录下。BASH2 提供了一些新功能,有兴趣的同叙可以自己去看相关资料,或直接 man bash2 即可。

    • do
      statments use $var
      done

      上面的语法结构在执行后,BASH 会将
  • 部分,则 day 将取遍命令行的所有参数。如这个程序
    #!/bin/bash
    for param
    do
    echo $param
    done

    exit 0
    上面这个程序将列出所有命令行参数。for 循环结构的循环体被包含在 do/done 对中,这也是后面的 while、until 循环所具有的特点。

    2.4.3 while

    while 循环的基本结构是:
    while [ condition ]
    do
    statments
    done

    这个结构请大家自己编写一个例子来验证。

    2.4.4 until

    until 循环的基本结构是:
    until [ condition is TRUE ]
    do
    statments
    done

    这个结构也请大家自己编写一个例子来验证。

    2.4.5 case

    BASH 中的 case 结构与 C 语言中的 switch 语句的功能比较类似,可以用于进行多项分支控制。其基本结构是:
    case “$var” in
    condition1 )
    statments1;;
    condition2 )
    statments2;;

    * )
    default statments;;
    esac

    下面这个程序是运用 case 结构进行分支执行的例子
    #!/bin/bash
    echo “Hit a key, then hit return.”
    read Keypress

    case “$Keypress” in
    [a-z] ) echo “Lowercase letter”;;
    [A-Z] ) echo “Uppercase letter”;;
    [0-9] ) echo “Digit”;;
    * ) echo “Punctuation, whitespace, or other”;;
    esac

    exit 0
    上面例子中的第四行 “read Keypress” 一句中的 read 语句表示从键盘上读取输入。这个命令将在本讲义的 BASH 的其他高级问题中讲解。

    2.4.6 break/continue

    熟悉 C 语言编程的都很熟悉 break 语句和 continue 语句。BASH 中同样有这两条语句,而且作用和用法也和 C 语言中相同,break 语句可以让程序流程从当前循环体中完全跳出,而 continue 语句可以跳过当次循环的剩余部分并直接进入下一次循环。

    2.5     函数的使用

    BASH 是一个相对简单的脚本语言,不过为了方便结构化的设计,BASH 中也提供了函数定义的功能。BASH 中的函数定义很简单,只要向下面这样写就可以了:
    function my_funcname {
    code block
    }

    或者
    my_funcname() {
    code block
    }

    上面的第二种写法更接近于 C 语言中的写法。BASH 中要求函数的定义必须在函数使用之前,这是和 C 语言用头文件说明函数方法的不同。
    更进一步的问题是如何给函数传递参数和获得返回值。BASH 中函数参数的定义并不需要在函数定义处就制定,而只需要在函数被调用时用 BASH 的保留变量 $1 $2 … 来引用就可以了;BASH 的返回值可以用 return 语句来指定返回一个特定的整数,如果没有 return 语句显式的返回一个返回值,则返回值就是该函数最后一条语句执行的结果(一般为 0,如果执行失败返回错误码)。函数的返回值在调用该函数的程序体中通过 $? 保留字来获得。下面我们就来看一个用函数来计算整数平方的例子:
    #!/bin/bash
    square() {
    let “res = $1 * $1″
    return $res
    }

    square $1
    result=$?
    echo $result

    exit 0


    BASH 中的特殊保留字

    3.1     保留变量

    BASH 中有一些保留变量,下面列出了一些:
    $IFS  这个变量中保存了用于分割输入参数的分割字符,默认识空格。
    $HOME  这个变量中存储了当前用户的根目录路径。
    $PATH  这个变量中存储了当前 Shell 的默认路径字符串。
    $PS1  表示第一个系统提示符。
    $PS2  表示的二个系统提示符。
    $PWD  表示当前工作路径。
    $EDITOR 表示系统的默认编辑器名称。
    $BASH  表示当前 Shell 的路径字符串。
    $0, $1, $2, …
    表示系统传给脚本程序或脚本程序传给函数的第0个、第一个、第二个等参数。
    $#   表示脚本程序的命令参数个数或函数的参数个数。
    $$   表示该脚本程序的进程号,常用于生成文件名唯一的临时文件。
    $?   表示脚本程序或函数的返回状态值,正常为 0,否则为非零的错误号。
    $*   表示所有的脚本参数或函数参数。
    $@   和 $* 涵义相似,但是比 $* 更安全。
    $!   表示最近一个在后台运行的进程的进程号。

    3.2    随机数

    随机数是经常要用到的,BASH 中也提供了这个功能,请看下面这个程序:
    #!/bin/bash
    # Prints different random integer from 1 to 65536
    a=$RANDOM
    echo $a

    exit 0
    这个程序可以在每次执行的时候随机的打印出一个大小在 1 到 65536 之间的整数。

    3.3     运算符

    算术运算符
    + – * / % 表示加减乘除和取余运算
    += -= *= /= 同 C 语言中的含义

    位操作符
    << <<= >> >>= 表示位左右移一位操作
    & &= | |= 表示按位与、位或操作
    ~ ! 表示非操作
    ^ ^= 表示异或操作

    关系运算符
    < > <= >= == != 表示大于、小于、大于等于、小于等于、等于、不等于操作
    && || 逻辑与、逻辑或操作

    3.4     变量的特殊操作

    BASH 中还有一些对变量的简洁、快速的操作,大家还记得 “${var}” 和 “$var” 同样是对变量的引用吧,对 ${var} 进行一些变化就可以产生一些新功能:
    ${var-default} 表示如果变量 $var 还没有设置,则保持 $var 没有设置的状态,并返回后面的默认值 default。
    ${var=default} 表示如果变量 $var 还没有设置,则取后面的默认值 default。
    ${var+otherwise} 表示如果变量 $var 已经设置,则返回 otherwise 的值,否则返回空( null)
${var?err_msg} 表示如果变量 $var 已经设置,则返回该变量的值,否则将后面的 err_msg 输出到标准错误输出上。
请同学们自己尝试下面的例子:
#!/bin/bash
echo ${var?There is an error}
exit 0
还有下面一些用法,这些用法主要用于从文件路径字符串中提取有用信息:
${var#pattern}, ${var##pattern} 用于从变量 $var 中剥去最短(最长)的和 pattern 相匹配的最左侧的串。
${var%pattern}, ${var%%pattern} 用于从变量 $var 中剥去最短(最长)的和 pattern 相匹配的最右侧的串。

另外 BASH  中还加入下面一些操作:
${var:pos} 表示去掉变量 $var 中前 pos 个字符。
${var:pos:len} 表示变量 $var 中去掉前 pos 个字符后的剩余字符串的前 len 个字符。
${var/pattern/replacement} 表示将变量 $var 中第一个出现的 pattern 模式替换为 replacement 字符串。
${var//pattern/replacement} 表示将变量 $var 中出现的所有 pattern 模式全部都替换为 replacment 字符串。


BASH 中的其他高级问题

4.1     BASH 中对返回值的处理

无论是在 Shell 中对 BASH 脚本返回值的处理,还是在脚本中对函数返回值的处理,都是通过 “$?” 系统变量来获得。BASH 要求返回值必须为一个整数,不能用 return 语句返回字符串变量。


4.2     用 BASH 设计简单用户界面

BASH 中提供了一个小的语句格式,可以让程序快速的设计出一个字符界面的用户交互选择的菜单,该功能就是由 select 语句来实现的,select 语句的语法为:
select var in
; do 。下面是一个运用 for 进行循环的例子:
#!/bin/bash
for day in Sun Mon Tue Wed Thu Fri Sat
do
echo $day
done

# 如果列表被包含在一对双引号中,则被认为是一个元素
for day in “Sun Mon Tue Wed Thu Fri Sat”
do
echo $day
done

exit 0
注意上面的例子中,在 for 所在那行的变量 day 是没有加 “$” 符号的,而在循环体内,echo 所在行变量 $day 是必须加上 “$” 符号的。另外如果写成 for day 而没有后面的 in
是 $var 需要遍历的一个集合,do/done 对包含了循环体,相当于 C 语言中的一对大括号。另外如果do 和 for 被写在同一行,必须在 do 前面加上 “;”。如: for $var in

do
statments
done

其中 $var 是循环控制变量。
-----------------------------------

Shell编程极简入门实践


0. 写在前面

程序员多多少少都会和命令行打交道,一些常用的命令,比如cdlsping等 等,使用起来可能问题不大。但大多数人对Shell编程的了解程度,可能仅止于那几个最常用的命令。当需要更复杂的命令或者需要写一个脚本来进行批处理的 时候,很多人可能感到头疼。而Unix/Linux复杂多变而又不太直观的命令常常让初学者望而却步。比如像这样的语句echo "123 abc" | sed 's/[0-9][0-9]*/& &/'的语句,再比如那些以-开头的参数(-l -n -a等等),即使从网上找到了可能符合自己要求的代码,也往往因为看不懂而无法修改化为己用。
这个极简教程,或者说笔记,针对的是正是这部分读者。具体地说,通过学习这篇文档,你将获得以下技能:
  • 熟练掌握Unix/Linux下的最常用命令及其最常见用法;
  • 能够编写脚本,对文件进行批处理,对一些网络任务进行自动化等等;
  • 避免写脚本过程中的最常见错误;
  • (Hopefully)可以借此消除对命令行的恐惧;
这个教程的特点是:
  • 不求全面,只求实用。只覆盖最常用的命令及其用法;
  • 以大量例子为导向;
  • 一边阅读一边动手写例程的话,大约只需要1.5-2.5小时的时间;
这篇文档假定你是在Linux/Unix环境下,比如Ubuntu, 比如Mac OS X。同时假定你至少了解一门其它的编程语言。这个教程的代码均在Mac OS下测试过,由于各种shell的标准差别很小,(有充足的理由相信)在别的平台应该也都能顺利运行。

1. Hello World

首先打开你用得最顺手的文本编辑器,在第一、二行分别打入
#!/bin/bash
echo "Hello, World!"
保存文件,文件可保存在你喜欢的文件夹,扩展名选择.sh,比如这样的文件:tutorial.sh
接着,打开命令行工具Terminal,首先将工作目录改到你保存文件的文件夹,比如如果你将tutorial.sh放在/Users/Steven/code,则在命令行里执行以下操作
cd /Users/Steven/code
cd是change directory的意思,因为我们要执行tutorial.sh这个脚本,所以我们要先将工作目录转到这个脚本对应的文件夹下面。接着,在命令行继续输入
chmod +x tutorial.sh
chmod是change mode,+x的意思是将tutorial.sh变为一个可执行的文件。
接下来,我们就可以运行tutorial.sh这个脚本了。在命令行里打入
./tutorial.sh
如无意外,你将看到命令行里返回Hello, World!这个字符串!请注意,文件名前面的./是必不可少的,它告诉系统,就在当前的目录查找一个叫tutorial.sh的文件,如果没有./,系统会只在系统目录里面查找(准确来说是PATH变量定义的路径)。
我们回头来看看tutorial.sh里面的程序,目前它只有两行:
#!/bin/bash
echo "Hello, World!"
#!是一个标记,它告诉系统该去哪里去寻找能“解释”tutorial.sh的解释器。 echo是回响的意思,意思是说echo后面的那一串东西,都会在命令行显示出来。它和其它语言的print是类似的。
就这样,我们完成了一个最简单的bash scripting的程序编写。这里面有几点需要注意:
  • 执行脚本文件前,先要cd到文件所在的目录;
  • 执行脚本文件前,先要chmod +x tutorial.sh将其变为可执行程序;
  • 脚本文件的第一行,记得写上#!/bin/bash。

2. 整数和字符串

变量的定义很简单,按照以下格式就可以了:
NAME=var
比如定义一个字符串:
NAME='Steven'
比如定义一个整型变量:
NUM=3
这里有几点要注意,一是变量的名字,虽然大小写不限,但按照惯例一般采用全大写的方式。第二点特别重要,让我们做一个小实验来说明一下。打开刚才的那个tutorial.sh文件,将之前的内容清空,并打入
#!/bin/bash
NAME = 'Steven'
echo $NAME
如果你是直接复制以上的代码段,那么命令行应该会出现以下错误信息:
./tutorial.sh: line 2: NAME: command not found
出现这个错误,是因为:定义变量时,=的前面和后面,都是不能有空格的!这一点可能和其它语言不一样,但请务必注意。因为出现这类错误时,报错信息定位的栏数(line 2),是指向你引用变量的那一段代码,而不是定义变量的那一行,因此debug起来可能不是那么直观。
于是,我们把代码改为:
#!/bin/bash
NAME='Steven'
echo $NAME
命令行将显示Steven
从上面的例子我们也可以看到,当你定义了一个变量,要引用它时,要在前面加上$。变量名两边可以加上花括号,比如这样${NAME}。花括号不是必须的,但最好养成加上的习惯。因为在某些情况下,不加花括号可能引发歧义[1]:
#!/bin/bash
NAME='Steven'
echo "My name is ${NAME}SLXie."
可想而知,如果没有花括号,NAME和后面的SLXie就无法区分了。
对于字符串变量,既可以用单括号,比如'Steven',也可以用双括号"Steven",它们之间有微妙而繁杂的区别,在这里我们先记住一点,当字符串里面包含对某个变量的引用时,必须用双括号。比如上面的"My name is ${NAME}SLXie."。请试着将它的双括号改为单括号,并观察它的输出结果。
单括号会将被引用字符串中的几乎所有特殊字符当作普通字符处理,比如上面的$,单括号时只把它当作一个普通的美元符号输出。
再看一个例子,试着在脚本分别输入这两行,并观察它们的输出。
echo "Here is $50."
echo 'Here is $50.'
在这里插播一句,看这个教程的时候,最好是看到那里,就动手写到哪里。写的时候,不要直接复制粘贴,而是试着手打代码到编辑器里面。有时候代码里有一些微小而琐碎的东西,一定要自己打一遍才能记得牢。比如,一开始可能容易将#!/bin/bash打成#/bin/bash,或者#!bin/bash
花括号{}也可以用来对字符串进行某些操作。比如下面这个例子:
#!/bin/bash
USERNAME='StevenSLXie'
echo "My name is ${USERNAME}. People usually call me ${USERNAME:0:6}."
它会输出:
My name is StevenSLXie. People usually call me Steven.
这时候,${USERNAME:0:6}的作用是取字符串的一部分。第一个数字0是指截取的起始部分,则第二个数字6则是指截取的长度。再比如${#USERNAME}则是获取字符串的长度。更多的字符串用法,我们将在后面的正则表达式哪一节看到。

3. 数组

数组可以这样简单粗暴地定义:
NAMES[0]='Steven'
NAMES[1]='Peter'
NAMES[2]='David'
当数组体量太大时,这样定义未免麻烦,在bash里面(01-Jan-2015更新,感谢@mzhboy提醒,其它的一些shell,如zsh、ash不能用这种方法。)我们也可以用一行声明的方式来定义:
declare -a NAMES=('Steven' 'Peter' 'David')
declare -a来声明,后面一次性定义所有数组元素。请注意,在这里,整个数组用小括号括起来,而每个数组元素之间,是用空格来隔开的,而不是逗号或者其它。
访问数组的其中一个元素,和其它语言没什么不同。在你声明好数组之后,就可以访问数组元素了:
echo ${NAMES[0]}
echo ${NAMES[2]}
echo ${NAMES[*]}
${NAMES[*]}或者${NAMES[@]}表示访问所有元素。而${#NAMES[*]}则返回数组长度。请注意它和${#NAMES}的区别,后者是返回NAMES里面第一个元素的长度,相当于${#NAMES[0]}
一个数组声明并定义后,我们仍可以二次定义它,比如下面的代码是在原来的数组基础上再添加一个人名。
declare -a NAMES=('Steven' 'Peter' 'David')
echo ${#NAMES[*]}
NAMES=("${NAMES[*]}" 'Nancy')
echo ${NAMES[*]}
命令行将返回:
3
Steven Peter David Nancy

4. 运算符

4.1 算术运算符
Shell编程里的算术运算符和大多数编程语言很类似,主要是这些+ - * / %等。如果你试着在命令行里执行运算的话,比如输入以下算式:
2 + 2
会得到:
-bash: 2: command not found
这条错误信息。这是因为命令行的逻辑是它会把一行命令的第一个词当作是命令,在系统中寻找与之匹配的执行语句,因为在这里它会认为2是一个命令,而显然它不可能找到这个命令。要想执行运算,我们在命令行里打入
expr 2 + 2
输出结果是4expr是一个常用命令,evaluate an expression的意思。注意,这里数字和运算符之间,必须有一个空格。不然的话,如果你输入,
expr 2+2
则会输出
2+2
这种情况下,expr会把后面的2+2当成一个字符串,而evaluate一个字符串的结果,自然就是它本身了。算术运算当然也可以用变量,比如:
VAR=5
expr 2 + ${VAR}
其它的算术运算符大体类似,但有一个要特别注意,如果你进行乘法运算,比如:
expr 2 * 15
会输出:
expr: syntax error
这是因为*是一个特殊的字符(后面还会介绍到),而当要表达其原来的意思是,我们需要在它前面加上下划线\。就像这样:
expr 2 \* 15
(31-Dec-2014更新。感谢@mzhboy补充。)此外还有let也是挺常用的。let计算跟在它后面的表达式。和let极其类似的还有(())。看下面的例子:
A=3
B=4

let add=$A+$B
((sub=$B-$A))

echo $add
echo $sub

除此之外还有一个强大的计算命令bc,这里就暂不展开了。有兴趣的同学可以看这里
4.2 关系判断运算符
Shell提供了丰富的关系判断运算符,先来看一个例子,在tutorial.sh加入以下代码:
A=10
B=15

if [ $A -eq $B ]
then
    echo 'True'
else
    echo 'False'
fi
这是一个if条件判别语句(后面再细讲)。-eq判断运算符左右两边是否相等,如果是,则返回True,不然就返回False。关系判断运算符的基本格式是[ VAR1 OPERATOR VAR2 ],用一个中括号括起来,这里有一点细节要注意,中括号和变量之间,需要有空格隔开。所以像[$A -eq $B]是会报错的。(不如自己写段代码试一试?)
完整的关系判断运算符文档可以看这里:Unix Basic Operator
4.3 逻辑运算符
Shell编程里,与逻辑和或逻辑分别是用-a-o来表示的(02-Jan-2015更新。感谢@星光 修正)。看看下面这个例子:
A=10
B=15

if [ $A -lt 8 -a $B -gt 8 ]
then
    echo 'True'
else
    echo 'False'
fi
这是判断变量A是否小于8且变量B是否大于B。
(31-Dec-2014更新。感谢@mzhboy指出。)除了-o-a&&||也可以来表示与和或,但在表达上稍稍有一点不一样:每一个条件就要用一个中括号括起来。比如:
if [ $A -lt 8 ] && [ $B -gt 8 ]
then
    echo 'True'
else
    echo 'False'
fi
完整的关系判断运算符文档可以看这里:Unix Basic Operator

5. if条件判断、while循环、for循环

if | while | for语句和其它主流语言很相似,因此用起来应该不是大问题。值得注意的可能是以下几个小点:
  • if语句的格式是if...then...elif...fi。注意,这里用fi来标记一个条件判断的结束。嗯,感觉是一种很调皮的设定。
  • 所有的while和for语句,其执行的语句都始于do,终于done。
  • for的格式是for VAR in ARRAY,是Python的样式,和经典的C for循环可能稍有不同。
下面我们用一个例子来感受一下这三种语句。
echo 'Shall we begin the demo?(y/n)'
read ANS

A=0
declare -a B=(0)

if [ $ANS = 'y' ]
then
    echo 'Output the results of the while loop'
    while [ $A -lt 10 ]
    do
        echo $A
        A=`expr $A + 1`
        B=(${B[*]} $A)
    done
else
    echo 'Not ready for the demo yet.'
fi

echo 'Output the results of the for loop.'
for I in ${B[*]}
    do
        echo $I
    done
第二行的read在这个文档是第一次出现,它的功能是从Terminal里读取一行,并将这一行内容赋给read后面的那个变量。在这个例子里,我们询问用户是否要开始demo,当得到肯定回答后,用户的回答(假定是y),将被赋给ANS这个变量。
接着,我们用一个if判断语句,如果用户回答是y,则执行while循环。while循环的内容比较简单,在每一次循环里,先打印出A的值,接着将A的值加一,然后将每一个得到的新的A放进数组B里面。当A大于等于10的时候,退出循环。
最后是一个for循环,for的功能是将数组B里面的元素依次打印出来。
这里要补充一点,在对A加一的时候,我们使用的是A=`expr $A + 1`,如果你试图使用类似A=$A + 1这样的赋值,将会报错。这是因为如前所述,每一个算术运算,都需要用expr来算出它的结果。那expr两边的` `又是怎么回事呢?这里的` `其实是命令输出的引用,我们知道expr $A + 1本身是一个命令语句,而当我们要引用这个语句的执行结果的时候,就要加上` `了。举一个比较容易理解的例子吧:如果你在命令行输入date,就会输出系统的当前时间,这时候date是一个命令,但如果你要将date这个命令的执行结果赋给一个变量D,就要D=`date`。同样的,如果我们要将5 + 25的结果赋给一个变量SUM,则需要SUM=`expr 5 + 25`
` `在键盘上的位置是在Tab上面,1的左边。
还有一点要注意,当我们对一个已经定义过的变量进行重新赋值的时候,是不需要加$的,上面的例子A=`expr $A + 1`B=(${B[*]} $A)里,等式左边的变量都不需要加$
(31-Dec-2014更新。感谢@mzhboy指出。)此外还有一种for的情况需要注意。我们看下面这段代码:
A="cat dog"

for I in $A
    do
        echo $I
    done

for I in "$A"
    do
        echo $I
    done
第一个echo的输出是:
cat
dog
第二个:
cat dog
也就是说当我们用双引号将$A括起来时,cat和dog中间的空格自动被忽视了,整个A被当做一个字符串看待。这一点要注意,通常这个情况下(因为我们用for来枚举),所以一般用第一种。

6. 函数

Shell脚本里当然可以定义函数。比如这样的:
hello(){
    echo "Hello World!"
}

函数可以直接调用,比如下面这个脚本:
#!/bin/bash
hello(){
    echo "Hello World!"
}

hello
注意,但函数没有参数时,调用只需要写上函数名,而不是hello()之类的。
Shell函数的特殊之处,在于它参数传递的形式,具体地说,参数并不是像别的语言一样,写在括号里面,而是类似下面这个例子:
intro(){
    echo "There are $1 people here. They are $2, $3."
}
Shell用$1 $2这样的特殊记号来标记函数参数。而$0则是函数名,$n表示第n个参数。调用上面的intro函数,则是这个样子:
intro 2 'Steven' 'David'
命令行的输出则是:
There are 2 people here. They are Steven, David.
参数不是写在括号里,而是在函数名之后依次排列,并以空格隔开。
函数也可以有返回值,比如:
hello(){
    A=`expr 5 \* 10`
    return $A
}
返回变量A的值。请注意,这里获取返回值的方式和其它语言可能不太一样。并不是A=hello,而是:
hello
RET=$?
$?是一个特殊字符,它保留上一个命令的执行结果。因此,当我们要获取hello函数的返回值时,就在调用该函数后,紧接着将$?赋给存储返回值的变量ret。在这里,这两句命令是必须紧挨着的,如果中间还有其它语句,则$?会返回最近一次的命令执行结果,而不一定是hello的返回值。

7. sed和正则表达式

正则表达式是一种特殊的字符串,用来描述一串具有某种共同特征的字符串。在进行批处理的时候,正则表达式有着异常强大的应用。
sed则是一个流编辑器(stream editor),它读入一个输出,并通过加工处理,输出经处理后的 文件/字符串 输出。下面我们通过一系列例子,来掌握sed的基本应用。
首先我们要来新建一些txt文件供sed处理。在命令行输入:
mkdir files
cd files
touch test-1.txt
touch test-2.txt
touch test-3.txt
cd ../
第一行mkdir是在当前目录下新建一个叫files的文件夹。然后我们cd到新的文件夹里,在里面用touch新建三个txt文档。接着,通过cd ../回到上级目录,也就是我们的tutorial.sh所在的目录里。
然后,分别打开那三个txt文件,将以下几行字符串拷贝到文件里。
This is a file with several lines
some of which are blank lines
for example, the line that follows is blank

But this line has several characters.

And this marks the end of the file.
接着,打开我们的tutorial.sh,将之前内容清空,只留下第一行的#!/usr/bash。在里面输入:
cd files
FILE=test-1.txt

sed -i.tmp "/^$/d" $FILE
保存后运行./tutorial.sh。然后打开test-1.txt,如无意外的话,你会看到文档变成这个样子:
This is a file with several lines
some of which are blank lines
for example, the line that follows is blank
But this line has several characters.
And this marks the end of the file.
所有的空行被删除了。我们来看看sed -i.tmp "/^$/d" $FILE这句命令。其中"/^$/d"描述了sed作用于什么模式(pattern),以及操作(action),它的基本结构是这样的"/pattern/action"。其中pattern里面是一个正则表达式,表面了我们要寻找什么样的字符串(pattern match),找到之后,则是对这个字符串进行操作(action)。在我们这个例子里,我们的正则表达式是"^$",其中^是标记一个字符串的开始,而$是标记一个字符串的结束。在这里,开始和结束连在一起,表示我们要寻找的是一个空字符串,而对它的操作是ddelete的意思。整个句子合起来就是,sed会打开test-1.txt,逐行扫描,找到空行,删除掉空行。
sed前面的 -i则是in place的意思,就是说我们这个操作是直接对目标文件下手的。这样的修改通常是比较危险的(万一改错了呢),所以一个安全的办法是在-i后面加上-i.tmp,意思是每次改动,我们都会保留一个后缀为.tmp的备份文件。于是你打开文件夹的话,会看到多出了一个test-1.txt.tmp。如果你确认你的改动没有问题之后,就可以把这个备份文件删掉了。备份文件的扩展名可以随意,因为当你需要它的时候总是需要把它改回txt的。但最好扩展名不要和文件夹的已有文件重复。
接着你可以试试将上面的sed语句改为:
sed -e "/^$/d" test-2.txt
这时你会发现改动后的结果,在命令行显示出来了,而原文件并不会改动。
再来看下面的语句:
sed -e "1,3d" test-2.txt
这个语句的意思,就是删除test-2.txt中的第一到第三行。观察输出可以看到,前三行被删除了。类似地,sed -e "2,4!d" test-2.txt则是指,除了2-4行,其他都删掉。sed -e "d" test-2.txt 则是删除文档里面的全部内容。请注意,为了避免频繁改动文档,以上几个命令都是用的-e,改动是体现在命令行的输出的。如果要直接对文档进行改动,请用-i.tmp,或者分开写为-i '.tmp'。在Mac OS X的Bash Shell里面,似乎提供一个备份文件的扩展名是必须的,而在Linux平台则似乎是可选的。
更为常见的sed的应用,是用它来进行替换。看看下面的例子:
sed -e 's/But/but/' test-2.txt
命令行的输出:
This is file with several lines
some of which are blank lines
for example, the line that follows is blank

but this line has several characters.

And this marks the end of the file.
可以观察到,首字母大写的But被替换成but。替换的基本格式是's/old/new/'oldnew分别代表替换前和替换后,而且一般用单引号,除非需要shell变量展开到sed命令中才用双引号(31-Dec-2014,感谢@mzhboy补充)。我们再来多看几个例子,首先将test-2.txt文档的内容改为:
1.This is file with several lines
2.some of which are blank lines
 for example, the line that follows is blank

7.But this line has several characters.

 And this marks the end of the file.
第三行和最后一行前面有一个空格。接着我们在tutorial.sh里面加入以下命令:
sed -i '.tmp' 's/^[ 1-3]//' test-2.txt
你会看到以下输出:
.This is file with several lines
.some of which are blank lines
for example, the line that follows is blank

7.But this line has several characters.

And this marks the end of the file.
在这个例子里,正则表达式^[ 1-3]^表示字符串的开始,而中括号表示匹配任意一个在中括号里面的字符。我们的中括号里面有空格以及数字1-3,而之后*表示零到任意多个任意字符。于是,sed根据正则表达式的要求去逐行扫描,找出“以空格或者数字1-3开头的行”。
找到之后干什么呢?这就要看第二个/后面的内容了,而我们发现第二个/和第三个/之间并没有内容。这是说,找到了符合要求的这个字符串,就将其替换为空字符。于是你可以看到以上的输出了。第一行第二行的数字被移除,第三行和最后一行的空格被移除,而第五行的数字7则不受影响。
保留新的test-2.txt,我们继续执行以下命令:
sed -i '.tmp' 's/[!.]$/;/' test-2.txt
这个命令则是找出以! .结尾的行,并一律改为以分号结尾。
我们继续在原有的文档操作,输入以下命令:
sed -i '.tmp' 's/^[. 1-9]*//;s/[;.!]$/ ENDING/' test-2.txt

这是两个替换命令一起来。首先,我们找出以. 或者数字1-9,或者空格开头的行,然后将其删掉。请注意,这里有一个*,它代表了任意多个(包括0)前一个字符的重复。比如如果一个行是以三个空格开头的,则加上*可以加之一并铲除,如果不加的话就只会匹配并删除第一个空格。两个合并命令之间以空格隔开。第二个命令是找出以分号句号或者感叹号结尾的行,代之以ENDING
输出结果应该是:
This is file with several lines
some of which are blank lines
for example, the line that follows is blank

But this line has several characters ENDING

And this marks the end of the file ENDING
回头看这个命令,sed -i '.tmp' 's/^[. 1-9]*//;s/[;.!]$/ ENDING/' test-2.txt。两个小时之前,如果你看到这么复杂的命令行的时候,很有可能因为看起来过于复杂而崩溃掉吧。但现在,你也能读、写这样复杂的命令了。恭喜!
让我们继续来看看sed的一些其它应用。
sed -i '.tmp' 's/^some.*//' test-2.txt
删除以some开头的行。其中.可以指代任意字符。
sed -i '.tmp' 's/....$//' test-2.txt
删除每行末尾的四个字符。
sed -i '.tmp' 's/But/but/g' test-2.txt
这个看起来有点熟悉对不对?如果你比较之前的语句,会发现多了一个g。在这里,如果不加g则只会替换每行的第一个But,而加g则会替换所有的But。如果你只是要替换某一个位置的But,比如第三个,则可以sed -i '.tmp' 's/But/but/3' test-2.txt
除了删除和替换,sed还支持插入(insert/append)新的字符串。
比如下面的例子:
sed -i '.tmp' '3 a\
just some random text' test-2.txt
它的功能是往test-2.txt的第三行后面添加just some random text这一新行。注意a\之后要另起一行,这里aappend的意思。如果a\改为i\,则是在前面加新行。iinsert的意思。
另外一个例子:
sed -i '.tmp' '$ a\
just some random text' test-2.txt
在文件末尾处加上新行。
当然我们也可以用正则表达式要判别,比如:
sed -i '.tmp' '/text/ i\
INSERT THIS BEFORE EVERY LINE CONTAINING TEXT' test-2.txt
在每一行包含text的行前面加入INSERT THIS BEFORE EVERY LINE CONTAINING TEXT
更多的sed用法,可以参考:http://www.grymoire.com/Unix/Sed.html#uh-1

9. grep

grep相当于Unix/Linux命令行的Google,可以快速地找出包含某个字符串的文件。让我们先将当前目录移到files子文件夹,并显示该目录下的所有文件名。
cd files
ls
如果你按照这个教程一路走下来,现在这个文件夹里面应该有test-1.txt test-2.txt test-3.txt几个文件,以及可能的备份文件。如果不是也不要紧。接下来我们执行一个grep,来找出test-1.txt里面包含'file'的那些行。
grep "file" test-1.txt
正常的话会输出:
This is file with several lines
And this marks the end of the file.
这两行包含着file
grep可以同时搜索多个文件,比如这样:
grep "file" test-1.txt test-2.txt
输出则是:
test-1.txt:This is file with several lines
test-1.txt:And this marks the end of the file.
test-2.txt:This is file with several li
test-2.txt:And this marks the end of the file END
格式是文件名:字符串。当然,罗列所有的文件名,有时候很不方便。这时候可以用上模糊搜索。比如这样:
grep "file" test-*.txt
有时候你只想知道哪些文件包含了某个字符串,而对那一行的具体内容是什么并不重要,那么可以这样:
grep -l "file" test-*.txt
系统会打印出包含file的文件名。有的时候你想多知道一点:不想打印出整行字符串,但想知道每个文件有几行包含file,那么可以:
grep -c "file" test-*.txt
而有时候你想知道的非常多,不仅是文件名,出现了几行,而且具体的行数也要知道,那么可以:
grep -n "file" test-*.txt
有的时候你想知道输出相反的结果:那些不包含file的行,那么可以:
grep -vn "file" test-*.txt
grep当然也支持正则表达式。要知道,grep的全称就是global regular expression print。所以“你问我支持不支持,他的名字叫全局正则表达式打印器,怎么能不支持?”(请忽略一个蛤丝的老梗~)。
我们来看几个例子:
grep "END$" test-*.txt
输出结尾为END的那些行。
grep "file\|But" test-*.txt
输出含有file或者But的行。注意|前面需要用\
如果我们要搜索包含characters.的行,正确的命令是这样的:
grep 'characters\.' test-*.txt
同时试一试grep 'characters.' test-*.txt,看看结果有什么不同。

10. 写一个脚本吧

前面九节基本上覆盖了Shell编程最基本的内容,这一节我们来动手写一个脚本,一方面是把我们之前学到的东西复习一下,串联起来。另一方面,是将之前没有覆盖到的几个常用命令介绍一下。
我们的任务是在当前目录下,新建一个脚本final-script.sh。然后在脚本里新建一个文件夹final,在新文件夹里批量新建十个.txt文件,命名规则为final-test-n.txt。每个文件的文档内容"This is a test file...!!"
接着,将十个文件的文件名从final-test-n.txt改为Final-test-n.txt
然后,删除文件内容里面的标点符号。
接着,将文件内容全部变成大写。
将改动后的文件,拷贝一份到任意一个新的文件夹。
请注意,这当中会出现一些我们没学到的知识,如果你发现现有的知识不足以解决问题的话,请Google之。
下面是我写的脚本,为了便于理解,我将各个命令都分开写了。
#!/bin/bash
mkdir final
cd final

declare -a NAME
NAME=(1 2 3 4 5 6 7 8 9 10)


# 创建新文件。
for I in ${NAME[*]}
    do
        touch final-test-${I}.txt
    done

# 往文档写入。这里使用的是echo,通过`>`改变其默认输出。不妨思考一下如果用sed来实现会有什么问题?
for F in final-test-*.txt
    do
        echo 'This is a test file...!!' > $F
    done

# 文件名首字母大写。注意echo和sed的连用,以及我们引用命令的一种新方法$(command)。还有mv这个新命令。
for F in final-test-*.txt
    do
        NEW=$(echo "$F" | sed -e 's/^./F/')
        mv "$F" "$NEW"
    done

# 删除标点。
for F in *.txt
    do
        sed -i.tmp 's/...!!$//' $F
    done

# 大写。注意tr的使用。
for F in *.txt
    do
        tr '[:lower:]' '[:upper:]' < $F > FILE2
        mv FILE2 $F
    done

cd ../
mkdir repo

cd final

# 复制文件。
for F in *.txt
    do
        cp $F ../repo/$F
    done
当中新出现的东西,就留待读者自己解决啦。如果你也完成了这个任务,不妨将你的代码发给我。相信你一定能写出比我更简洁的代码!

11. 后记

Happy Coding!

参考资源

勘误和交流

匆忙写下的一个笔记,出错简直是一定的。如果您发现了任何错误或者有关于本文的任何建议,麻烦发邮件给我(stevenslxie at gmail.com)或者在GitHub上直接交流,不胜感激.

FROM https://github.com/StevenSLXie/Tutorials-for-Web-Developers/blob/master/Shell%E7%BC%96%E7%A8%8B%E6%9E%81%E7%AE%80%E5%85%A5%E9%97%A8%E5%AE%9E%E8%B7%B5.md