Shell程序设计

本文其实是《Linux程序设计》第二章第六节的内容,原文点这里。这里之所以搬过来,只是对自己学习的记录而已。

变量

  • 所有的变量都以字符串来存储,即便其值为数值也是如此。
  • 使用$对变量取值
  • 给变量赋值时等号两边不能有空格
  • 如果字符串里面包含空格,必须使用引号引起来
$ salutation=Hello
$ echo $salutation
Hello
$ salutation="Yes Dear"
$ echo $salutation
Yes Dear
$ salutation=7+5
$ echo $salutation
7+5

输入

$ read salutation
Wie geht's?
$ echo $salutation
Wie geht's?

使用引号

双引号中的变量才能使用$进行求值,而单引号中的变量求值被忽略。
示例脚本:

#!/bin/sh

myvar="Hi there"

echo $myvar
echo "$myvar"
echo '$myvar'
echo \$myvar

echo Enter some text
read myvar

echo '$myvar' now equals $myvar
exit 0

输出结果如下:

$ ./variable
Hi there
Hi there
$myvar
$myvar
Enter some text
Hello World
$myvar now equals Hello World

反斜杠可以对$进行转义,阻止了变量的求值。

环境变量

当一个shell脚本开始执行时,有一些预先初始化的变量,叫做环境变量。这些变量通常用大写字母命名,以便和用户的脚本程序中的变量区分开来,后者按惯例都用小写字母做名字。一般系统中有这些环境变量。
这里写图片描述

参数变量

如果脚本程序在调用时带有参数,一些额外的变量就会被创建。即使没有传递任何参数,环境变量$#也依然存在,只不过它的值是0罢了。
这里写图片描述
一般来说,访问脚本程序的参数都使用$@

脚本实例:

#!/bin/sh

salutation="Hello"
echo $salutation
echo "The program $0 is now running"
echo "The second parameter was $2"
echo "The first parameter was $1"
echo "The parameter list was $*"
echo "The user's home directory is $HOME"

echo "Please enter a new greeting"
read salutation

echo $salutation
echo "The script is now complete"
exit 0

输出结果:

$ ./try_var foo bar baz
Hello
The program ./try_var is now running
The second parameter was bar
The first parameter was foo
The parameter list was foo bar baz
The user's home directory is /home/rick
Please enter a new greeting
Sire
Sire
The script is now complete
$

条件测试

if,for,while等控制结构需要进行条件测试,一般的编程语言中使用布尔表达式来进行条件测试。而shell中只能使用特殊的命令,根据这些特殊命令的返回值进行判断。这也就是脚本程序在结尾处要使用exit命令给环境返回一个状态码。

test[命令

这两个命令其实功能是一样的,只不过后者使得脚本更易读。
例如,检查一个文件是否存在。可以这样写:

if test -f fred.c
then
...
fi

还可以这样:

if [ -f fred.c ]
then
...
fi

if检查test命令的退出码来决定是否需要执行后面的代码。注意,[和被检查的条件之间需要空格,因为这是一个跟test一样的命令。

then也可以和if放到同一行上,那么就必须用一个分号把test语句和then分隔开。如下:

if [ -f fred.c ]; then
...
fi

test命令可以使用的条件类型分为3类:字符串比较、算术比较和与文件有关的条件测试,如下表所示:

字符串比较结果
string1 = string2如果两个字符串相同则结果为真
string1 != string2如果两个字符串不同则结果为真
-n string如果字符串不为空则为真
-z string字符串为null(空串)则为真
算术比较结果
expression1 -eq expression2相等
expression1 -ne expression2不等
expression1 -gt expression2大于
expression1 -ge expression2大于等于
expression1 -lt expression2小于
expression1 -le expression2小于等于
文件条件测试结果
-d file如果是目录
-e file文件存在(不可移植,一般用-f)
-f file如果文件是一个普通文件
-g file如果文件的set-group-id位被设置
-r file如果文件可读
-s file如果文件大小不为0则结果为真
-u file如果文件的set-user-id位被设置
-w file如果文件可写
-x file如果文件可执行

set-user-id位授予了程序其拥有者的访问权限而不是其使用者的访问权限,而set-group-id位授予了程序其所在组的访问权限。
脚本实例:

#!/bin/sh

if [ -f /bin/bash ]
then
  echo "file /bin/bash exists"
fi

if [ -d /bin/bash ]
then
  echo "/bin/bash is a directory"
else
  echo "/bin/bash is NOT a directory"
fi

控制结构

if语句

脚本实例:

#!/bin/sh

echo "Is it morning? Please answer yes or no"
read timeofday

if [ $timeofday = "yes" ]; then
  echo "Good morning"
else
  echo "Good afternoon"
fi

exit 0

输出结果:

Is it morning? Please answer yes or no
yes
Good morning
$

elif语句

脚本实例:

#!/bin/sh

echo "Is it morning? Please answer yes or no"
read timeofday

if [ $timeofday = "yes" ]
then
  echo "Good morning"

elif [ $timeofday = "no" ]; then
  echo "Good afternoon"
else
  echo "Sorry, $timeofday not recognized. Enter yes or no"
  exit 1
fi

exit 0

这个程序中有个问题,如果用户没有输入任何东西直接回车,程序运行会有错误,因为if语句变成了这样:

if [ = "yes" ]

这不是一个合法的条件测试。为了避免出现这种情况,你必须给变量加上引号,如下所示:

#!/bin/sh

echo "Is it morning? Please answer yes or no"
read timeofday

if [ "$timeofday" = "yes" ]
then
  echo "Good morning"
elif [ "$timeofday" = "no" ]; then
  echo "Good afternoon"
else
  echo "Sorry, $timeofday not recognized. Enter yes or no"

  exit 1
fi

exit 0

如果你想让echo命令去掉每一行后面的换行符,可移植性最好的办法是使用printf命令(请见本章后面的printf一节)而不是echo命令。有的shell用echo-e命令来完成这一任务,但并不是所有的系统都支持该命令。bash使用echo–n命令来去除换行符,所以如果确信自己的脚本程序只运行在bash上,你就可以使用如下的语法:

echo -n "Is it morning? Please answer yes or no: "

for语句

脚本实例:

#!/bin/sh

for foo in bar fud 43
do
  echo $foo
done
exit 0

输出结果如下:

bar
fud
43

脚本实例:

#!/bin/sh

for file in $(ls f*.sh); do
  lpr $file
done
exit 0

正如 ()用来表示一个命令的输出。

while语句

for适合于对一系列字符串进行循环处理,但如果事先不知道循环要执行的次数,那么while就更有用了。
请看下面的例子,这是一个非常简陋的密码检查程序:

#!/bin/sh

echo "Enter password"
read trythis

while [ "$trythis" != "secret" ]; do
  echo "Sorry, try again"
  read trythis
done
exit 0

输出如下:

Enter password
password
Sorry, try again
secret
$

until语句

它与while循环很相似,只是把条件测试反过来了。换句话说,循环将反复执行直到条件为真,而不是在条件为真时反复执行。

下面是一个until循环的例子,你设置一个警报,当某个特定的用户登录时,该警报就会启动,你通过命令行将用户名传递给脚本程序。如下所示:

#!/bin/bash

until who | grep "$1" > /dev/null
do
   sleep 60
done

# now ring the bell and announce the expected user.

echo -e '\a'
echo "**** $1 has just logged in ****"

exit 0

如果用户已经登录,那么循环就不需要执行。所以在这种情况下,使用until语句比使用while语句更自然。

case语句

语法如下所示:

case variable in
  pattern [ | pattern] ...) statements;;
  pattern [ | pattern] ...) statements;;
  ...
esac

请注意,每个模式行都以双分号(;;)结尾。因为你可以在前后模式之间放置多条语句,所以需要使用一个双分号来标记前一个语句的结束和后一个模式的开始。

实例一,用户输入:

#!/bin/sh

echo "Is it morning? Please answer yes or no"
read timeofday

case "$timeofday" in
    yes)  echo "Good Morning";;
    no )  echo "Good Afternoon";;
    y  )  echo "Good Morning";;
    n  )  echo "Good Afternoon";;
    *  )  echo "Sorry, answer not recognized";;
esac

exit 0

实例二,合并匹配模式:

#!/bin/sh

echo "Is it morning? Please answer yes or no"
read timeofday

case "$timeofday" in
    yes | y | Yes | YES )      echo "Good Morning";;
    n* | N* )                  echo "Good Afternoon";;
    * )                        echo "Sorry, answer not recognized";;
esac

实例三,执行多条语句:

#!/bin/sh

echo "Is it morning? Please answer yes or no"
read timeofday

case "$timeofday" in
    yes | y | Yes | YES )
           echo "Good Morning"
           echo "Up bright and early this morning"
           ;;
    [nN]*)
           echo "Good Afternoon"
           ;;
    *)
           echo "Sorry, answer not recognized"
           echo "Please answer yes or no"
           exit 1
           ;;
esac

exit 0

命令列表

有时,你想要将几条命令连接成一个序列。例如,你可能想在执行某个语句之前同时满足好几个不同的条件,如下所示:

if [ -f this_file ]; then
    if [ -f that_file ]; then
        if [ -f the_other_file ]; then
            echo "All files present, and correct"
        fi
    fi
fi

或者你可能希望至少在这一系列条件中有一个为真,像下面这样:

if [ -f this_file ]; then
   foo="True"
elif [ -f that_file ]; then
   foo="True"
elif [ -f the_other_file ]; then
   foo="True"
else
   foo="False"
fi
if [ "$foo" = "True" ]; then
   echo "One of the files exists"
fi

虽然这可以通过使用多个if语句来实现,但如你所见,写出来的程序非常笨拙。shell提供了一对特殊的结构,专门用于处理命令列表,它们是AND列表和OR列表。虽然它们通常在一起使用,但我们将分别介绍它们的语法。

AND列表

在下面的脚本程序中,你执行touch file_one命令(检查文件是否存在,如果不存在就创建它)并删除file_two文件。然后用AND列表检查每个文件是否存在并通过echo命令给出相应的指示。

#!/bin/sh

touch file_one
rm -f file_two

if [ -f file_one ] && echo "hello" && [ -f file_two ] && echo " there"
then
    echo "in if"
else
    echo "in else"
fi

exit 0

输出结果:

hello
in else

OR列表

脚本实例:

#!/bin/sh

rm -f file_one

if [ -f file_one ] || echo "hello" || echo " there"
then
    echo "in if"
else
    echo "in else"
fi

exit 0

输出结果:

hello
in if

语句块

如果你想在某些只允许使用单个语句的地方(比如在AND或OR列表中)使用多条语句,你可以把它们括在花括号{}中来构造一个语句块。例如,在本章后面给出的应用程序中,你将看到如下所示的代码:

get_confirm && {
   grep -v "$cdcatnum" $tracks_file > $temp_file
   cat $temp_file > $tracks_file
   echo
   add_record_tracks
}

函数

一个简单的函数:

#!/bin/sh

foo() {
    echo "Function foo is executing"
}

echo "script starting"
foo
echo "script ended"

exit 0

输出结果:

script starting
Function foo is executing
script ending

当一个函数被调用时,脚本程序的位置参数($*、$@、$#、$1、$2等)会被替换为函数的参数。这也是你读取传递给函数的参数的办法。当函数执行完毕后,这些参数会恢复为它们先前的值。

一些老版本的shell在函数执行之后可能不会恢复位置参数的值。所以如果你想让自己的脚本程序具备可移植性,就最好不要依赖这一行为。

你可以通过return命令让函数返回数字值。让函数返回字符串值的常用方法是让函数将字符串保存在一个变量中,该变量然后可以在函数结束之后被使用。此外,你还可以echo一个字符串并捕获其结果,如下所示:

foo () { echo JAY;}

...

result="$(foo)"

请注意,你可以使用local关键字在shell函数中声明局部变量,局部变量将仅在函数的作用范围内有效。此外,函数可以访问全局作用范围内的其他shell变量。如果一个局部变量和一个全局变量的名字相同,前者就会覆盖后者,但仅限于函数的作用范围之内。例如,你可以对上面的脚本程序进行如下的修改来查看执行结果:

#!/bin/sh

sample_text="global variable"

foo() {

    local sample_text="local variable"
    echo "Function foo is executing"
    echo $sample_text
}

echo "script starting"
echo $sample_text

foo

echo "script ended"
echo $sample_text

exit 0

如果在函数里没有使用return命令指定一个返回值,函数返回的就是执行的最后一条命令的退出码。

下一个脚本程序my_name演示了函数的参数是如何传递的,以及函数如何返回一个true或false值。你使用一个参数来调用该脚本程序,该参数是你想要在问题中使用的名字。

在shell头之后,我们定义了函数yes_or_no:

#!/bin/sh

yes_or_no() {
  echo "Is your name $* ?"
  while true
  do
    echo -n "Enter yes or no: "
    read x
    case "$x" in
      y | yes ) return 0;;
      n | no )  return 1;;
      * )       echo "Answer yes or no"
    esac
  done
}

然后是主程序部分:

echo "Original parameters are $*"

if yes_or_no "$1"
then
  echo "Hi $1, nice name"
else
  echo "Never mind"
fi
exit 0

这个脚本程序的典型输出如下所示:

$ ./my_name Rick Neil
Original parameters are Rick Neil
Is your name Rick ?
Enter yes or no: yes
Hi Rick, nice name
$

命令

你可以在shell脚本程序内部执行两类命令。一类是可以在命令提示符中执行的“普通”命令,也称为外部命令(external command),一类是我们前面提到的“内置”命令,也称为内部命令(internal command)。内置命令是在shell内部实现的,它们不能作为外部程序被调用。然而,大多数的内部命令同时也提供了独立运行的程序版本——这一需求是POSIX规范的一部分。通常情况下,命令是内部的还是外部的并不重要,只是内部命令的执行效率更高。

break命令

#!/bin/sh

rm -rf fred*
echo > fred1
echo > fred2
mkdir fred3
echo > fred4

for file in fred*
do
    if [ -d "$file" ]; then
        break;
    fi
done

echo first directory starting fred was $file

rm -rf fred*
exit 0

:命令

冒号(:)命令是一个空命令。它偶尔会被用于简化条件逻辑,相当于true的一个别名。由于它是内置命令,所以它运行的比true快,但它的输出可读性较差。

你可能会看到将它用作while循环的条件,while :实现了一个无限循环,代替了更常见的while true。

结构也会被用在变量的条件设置中,例如:

: ${var:=value}

如果没有:,shell将试图把$var当作一条命令来处理。

在一些shell脚本,主要是一些旧的shell脚本中,你可能会看到冒号被用在一行的开头来表示一个注释。但现代的脚本总是用#来开始一个注释行,因为这样做执行效率更高。

#!/bin/sh

rm -f fred
if [ -f fred ]; then
    :
else
    echo file fred did not exist
fi

exit 0

continue命令

也跟C类似,没啥说的

#!/bin/sh

rm -rf fred*
echo > fred1
echo > fred2
mkdir fred3
echo > fred4

for file in fred*
do
    if [ -d "$file" ]; then
          echo "skipping directory $file"
        continue
    fi
    echo file is $file
done

rm -rf fred*
exit 0

source命令

source命令和点命令,这两个命令差不多是同义词。
点(.)命令用于在当前shell中执行命令:

. ./shell_script

通常,当一个脚本执行一条外部命令或脚本程序时,它会创建一个新的环境(一个子shell),命令将在这个新环境中执行,在命令执行完毕后,这个环境被丢弃,留下退出码返回给父shell。但外部的source命令和点命令(这两个命令差不多是同义词)在执行脚本程序中列出的命令时,使用的是调用该脚本程序的同一个shell。

因为在默认情况下,shell脚本程序会在一个新创建的环境中执行,所以脚本程序对环境变量所作的任何修改都会丢失。而点命令允许执行的脚本程序改变当前环境。当你要把一个脚本当作“包裹器”来为后续执行的一些其他命令设置环境时,这个命令通常就很有用。例如,如果你正同时参与几个不同的项目,你就可能会遇到需要使用不同的参数来调用命令的情况,比如说调用一个老版本的编译器来维护一个旧程序。

在shell脚本程序中,点命令的作用有点类似于C或C++语言里的#include指令。尽管它并没有从字面意义上包含脚本,但它的确是在当前上下文中执行命令,所以你可以使用它将变量和函数定义结合进脚本程序。

下面的例子在命令行中使用点命令,但你完全可以把它用在一个脚本程序中。

假设你有两个包含环境设置的文件,它们分别针对两个不同的开发环境。为了设置老的、经典命令的环境,你可以使用文件classic_set,它的内容如下所示:

#!/bin/sh

version=classic
PATH=/usr/local/old_bin:/usr/bin:/bin:.
PS1="classic> "

对于新命令,使用文件latest_set:

#!/bin/sh

version=latest
PATH=/usr/local/new_bin:/usr/bin:/bin:.
PS1=" latest version> "

你可以通过将这些脚本程序和点命令结合来设置环境,就像下面的示例那样:

$ . ./classic_set
classic> echo $version
classic
classic> . /latest_set
latest version> echo $version
latest
latest version>

这个脚本程序使用点命令执行,所以每个脚本程序都是在当前shell中执行。这使得脚本程序可以改变当前shell中的环境设置,即使脚本程序执行结束后,这些改变仍然有效。

echo命令

虽然,X/Open建议在现代shell中使用printf命令,但我们还是依照常规使用echo命令来输出结尾带有换行符的字符串。

一个常见的问题是如何去掉换行符。遗憾的是,不同版本的UNIX对这个问题有着不同的解决方法。Linux常用的解决方法如下所示:

echo -n "string to output"

但你也经常会遇到:

echo -e "string to output\c"

第二种方法echo -e确保启用了反斜线转义字符(如\c代表去掉换行符,\t代表制表符,\n代表回车)的解释。在老版本的bash中,对反斜线转义字符的解释通常都是默认启用的,但最新版本的bash通常在默认情况下都不对反斜线转义字符进行解释。你所使用的Linux发行版的详细行为请查看相关手册。

如果你需要一种删除结尾换行符的可移植方法,则可以使用外部命令tr来删除它,但它执行的速度比较慢。如果你需要自己的脚本兼容UNIX系统并且需要删除换行符,最好坚持使用printf命令。如果你的脚本只需要运行在Linux和bash上,那么echo-n是不错的选择,虽然你可能需要在脚本的开头加上#!/bin/bash,以明确表示你需要bash风格的行为。

eval命令

eval命令允许你对参数进行求值。它是shell的内置命令,通常不会以单独命令的形式存在。我们借用X/Open规范中的一个小例子来演示它的用法:

foo=10
x=foo
y='$'$x
echo $y

它输出$foo,而

foo=10
x=foo
eval y='$'$x
echo $y

输出10。因此,eval命令有点像一个额外的$,它给出一个变量的值的值。

exec命令

exec命令有两种不同的用法。它的典型用法是将当前shell替换为一个不同的程序。例如:

exec wall "Thanks for all the fish"

脚本中的这个命令会用wall命令替换当前的shell。脚本程序中exec命令后面的代码都不会执行,因为执行这个脚本的shell已经不存在了。

exec的第二种用法是修改当前文件描述符:

exec 3< afile

这使得文件描述符3被打开以便从文件afile中读取数据。这种用法非常少见。

export命令

export命令将作为它参数的变量导出到子shell中,并使之在子shell中有效。在默认情况下,在一个shell中被创建的变量在这个shell调用的下级(子)shell中是不可用的。export命令把自己的参数创建为一个环境变量,而这个环境变量可以被当前程序调用的其他脚本和程序看见。从更技术的角度来说,被导出的变量构成从该shell衍生的任何子进程的环境变量。我们用下面两个脚本程序export1和export2来说明它的用法。

我们先列出脚本程序export2:

#!/bin/sh

echo "$foo"
echo "$bar"

然后是脚本程序export1。在这个脚本的结尾,我们调用了export2:

#!/bin/sh

foo="The first meta-syntactic variable"
export bar="The second meta-syntactic variable"

export2

如果你运行这个脚本程序,你将得到如下的输出:

$ ./export1

The second meta-syntactic variable
$

expr命令

expr命令将它的参数当作一个表达式来求值。它的最常见用法就是进行如下形式的简单数学运算:

x=`expr $x + 1`

反引号()字符使x取值为命令expr $x + 1的执行结果。你也可以用语法$()替换反引号,如下所示:

x=$(expr $x + 1)

expr命令的功能十分强大,它可以完成许多表达式求值计算。
在较新的脚本程序中,expr命令通常被替换为更有效的$((…))语法,这个我们会在本章后面的内容中介绍。

printf命令

只有最新版本的shell才提供printf命令。X/Open规范建议我们应该用它来代替echo命令,以产生格式化的输出,但看来几乎没什么人接受这一建议。

它的语法是:

printf "format string" parameter1 parameter2 ...

格式字符串与C/C++中使用的非常相似,但有一些自己的限制。主要是不支持浮点数,因为shell中所有的算术运算都是按照整数来进行计算的。格式字符串由各种可打印字符、转义序列和字符转换限定符组成。格式字符串中除了%和\之外的所有字符都将按原样输出。

return命令

return命令的作用是使函数返回。我们在前面介绍函数时已提到过它。return命令有一个数值参数,这个参数在调用该函数的脚本程序里被看做是该函数的返回值。如果没有指定参数,return命令默认返回最后一条命令的退出码。

set命令

set命令的作用是为shell设置参数变量。许多命令的输出结果是以空格分隔的值,如果需要使用输出结果中的某个域,这个命令就非常有用。

假设你想在一个shell脚本中使用当前月份的名字。系统本身提供了一个date命令,它的输出结果中包含了字符串形式的月份名称,但是你需要把它与其他区域分隔开。你可以将set命令和$(…)结构相结合来执行date命令,并且返回想要的结果。date命令的输出把月份字符串作为它的第二个参数:

#!/bin/sh

echo the date is $(date)
set $(date)
echo The month is $2

exit 0

这个程序把date命令的输出设置为参数列表,然后通过位置参数$2获得月份。

注意,我们以date命令作为一个简单的例子来说明怎么提取位置参数。由于date命令的输出受本地语言的影响较大,所以在实际工作中,你应该使用date +%B命令来提取月份名字。date命令还有许多其他格式选项,详细资料请参考它的手册页。

你还可以通过set命令和它的参数来控制shell的执行方式。其中最常用的命令格式是set -x,它让一个脚本程序跟踪显示它当前执行的命令。我们将在本章后面介绍程序调试时讨论set命令和它更多的选项。

shift命令

shift命令把所有参数变量左移一个位置,使$2变成$1$3变成$2,以此类推。原来$1的值将被丢弃,而$0仍将保持不变。如果调用shift命令时指定了一个数值参数,则表示所有的参数将左移指定的次数。$*、$@和$#等其他变量也将根据参数变量的新安排做相应的变动。

在扫描处理脚本程序的参数时,经常要用到shift命令。如果你的脚本程序需要10个或10个以上的参数,你就需要用shift命令来访问第十个及其后面的参数。

例如,你可以像下面这样依次扫描所有的位置参数:

#!/bin/sh

while [ "$1" != "" ]; do
    echo "$1"
    shift
done

exit 0

trap命令

trap命令用于指定在接收到信号后将要采取的行动,我们将在本书后面的内容中详细介绍信号。trap命令的一种常见用途是在脚本程序被中断时完成清理工作。历史上,shell总是用数字来代表信号,但新的脚本程序应该使用信号的名字,它们定义在头文件signal.h中,在使用信号名时需要省略SIG前缀。你可以在命令提示符下输入命令trap -l来查看信号编号及其关联的名称。

对于那些不熟悉信号的人们来说,信号是指那些被异步发送到一个程序的事件。在默认情况下,它们通常会终止一个程序的运行。

trap命令有两个参数,第一个参数是接收到指定信号时将要采取的行动,第二个参数是要处理的信号名。

trap command signal

请记住,脚本程序通常是以从上到下的顺序解释执行的,所以你必须在你想保护的那部分代码之前指定trap命令。

如果要重置某个信号的处理方式到其默认值,只需将command设置为-。如果要忽略某个信号,就把command设置为空字符串”。一个不带参数的trap命令将列出当前设置的信号及其行动的清单。

脚本实例:

#!/bin/sh

trap 'rm -f /tmp/my_tmp_file_$$' INT
echo creating file /tmp/my_tmp_file_$$
date > /tmp/my_tmp_file_$$

echo "press interrupt (CTRL-C) to interrupt ...."
while [ -f /tmp/my_tmp_file_$$ ]; do
    echo File exists
    sleep 1
done
echo The file no longer exists

trap INT
echo creating file /tmp/my_tmp_file_$$
date >  /tmp/my_tmp_file_$$

echo "press interrupt (control-C) to interrupt ...."
while [ -f /tmp/my_tmp_file_$$ ]; do
    echo File exists
    sleep 1
done

echo we never get here
exit 0

如果你运行这个脚本,在每次循环时按下Ctrl+C组合键(或任何你系统上设定的中断键),将得到如下所示的输出:

creating file /tmp/my_tmp_file_141
press interrupt (CTRL-C) to interrupt ....
File exists
File exists
File exists
File exists
The file no longer exists
creating file /tmp/my_tmp_file_141
press interrupt (CTRL-C) to interrupt ....
File exists
File exists
File exists
File exists

在这个脚本程序中,我们先用trap命令安排它在出现一个INT(中断)信号时执行rm –f /tmp/my_tmp_file_$$命令删除临时文件。脚本程序然后进入一个while循环,只要临时文件存在,循环就一直持续下去。当用户按下Ctrl+C组合键时,脚本程序就会执行rm –f /tmp/my_tmp_file_$$语句,然后继续下一个循环。因为临时文件现在已经被删除了,所以第一个while循环将正常退出。

接下来,脚本程序再次调用trap命令,这次是指定当一个INT信号出现时不执行任何命令。脚本程序然后重新创建临时文件并进入第二个while循环。这次当用户按下Ctrl+C组合键时,没有语句被指定执行,所以采取默认处理方式,即立即终止脚本程序。因为脚本程序被立即终止了,所以最后的echo和exit语句永远都不会被执行。

unset命令

unset命令的作用是从环境中删除变量或函数。这个命令不能删除shell本身定义的只读变量(如IFS)。这个命令并不常用。

下面的脚本第一次输出字符串Hello World,但第二次只输出一个换行符:

#!/bin/sh

foo="Hello World"
echo $foo

unset foo
echo $foo

使用foo=语句产生的效果与上面脚本中的unset命令产生的效果差不多,但并不等同。foo=语句将变量foo设置为空,但变量foo仍然存在,而使用unset foo语句的效果是把变量foo从环境中删除。

find和grep以及正则表达式

见原文

命令的执行

编写脚本程序时,你经常需要捕获一条命令的执行结果,并把它用在shell脚本程序中。也就是说,你想要执行一条命令,并把该命令的输出放到一个变量中。

你可以用在本章前面set命令示例中介绍的$(command)语法来实现,也可以用一种比较老的语法形式command,这种用法目前依然很常见。

$(command)的结果就是其中命令的输出。注意,这不是该命令的退出状态,而是它的字符串形式的输出结果。例如:

#!/bin/sh

echo The current directory is $PWD
echo The current users are $(who)

exit 0

因为当前目录是一个shell环境变量,所以程序的第一行不需要使用这个命令执行结构。但如果我们想要在脚本程序中使用who命令的输出结果,就需要使用这个结构。

如果想要将命令的结果放到一个变量中,你可以按通常的方法来给它赋值,如下所示:

whoisthere=$(who)
echo $whoisthere

这种把命令的执行结果放到变量中的能力是非常强大的,它使得在脚本程序中使用现有命令并捕获其输出变得很容易。如果需要把一条命令在标准输出上的输出转换为一组参数,并且将它们用做为另一个程序的参数,你会发现命令xargs可以帮你完成这一工作。具体细节请参考它的手册页。

有时,当你打算调用的命令在输出你想要的内容之前先输出了一些空白字符,或者它输出的内容比你想要的要多的时候也会出现问题。此时,你可以用前面介绍的set命令来解决。

算术扩展

我们已经介绍过expr命令,通过它可以处理一些简单的算术命令,但这个命令执行起来相当慢,因为它需要调用一个新的shell来处理expr命令。

一种更新更好的办法是使用$((...))扩展。把你准备求值的表达式括在$((…))中能够更有效地完成简单的算术运算。如下所示:

#!/bin/sh

x=0
while [ "$x" -ne 10 ]; do
    echo $x
    x=$(($x+1))
done

exit 0

注意,这与x=$(…)命令不同,两对圆括号用于算术替换,而我们之前见到的一对圆括号用于命令的执行和获取输出。

参数扩展

假设你想编写一个简短的脚本程序,来处理名为1_tmp和2_tmp的两个文件。你可以这样写:

#!/bin/sh

for i in 1 2
do
    my_secret_process ${i}_tmp
done

你可以在shell中采用多种参数替换方法。对于多参数处理问题来说,这些方法通常会提供一种精巧的解决方案。表中列出了一些常见的参数扩展方法。
这里写图片描述
脚本实例:

#!/bin/sh

unset foo
echo ${foo:-bar}

foo=fud
echo ${foo:-bar}

foo=/usr/bin/X11/startx
echo ${foo#*/}
echo ${foo##*/}

bar=/usr/local/etc/local/networks
echo ${bar%local*}
echo ${bar%%local*}

exit 0

输出结果如下:

bar
fud
usr/bin/X11/startx
startx
/usr/local/etc/
/usr/

这个地方原文有误,注意分辨。

假设你想使用cjpeg程序将一个GIF文件转换为一个JPEG文件:

$ cjpeg image.gif > image.jpg

但有时,你可能希望对大量文件执行这类操作,那么如何实现自动重定向呢?很简单,像下面这样做即可:

#!/bin/sh

for image in *.gif
do
  cjpeg $image > ${image%%gif}jpg
done

here文档

在shell脚本程序中向一条命令传递输入的一种特殊方法是使用here文档。它允许一条命令在获得输入数据时就好像是在读取一个文件或键盘一样,而实际上是从脚本程序中得到输入数据。

here文档以两个连续的小于号<<开始,紧跟着一个特殊的字符序列,该序列将在文档的结尾处再次出现。<<是shell的标签重定向符,在这里,它强制命令的输入是一个here文档。这个特殊字符序列的作用就像一个标记,它告诉shell here文档结束的位置。因为这个标记序列不能出现在传递给命令的文档内容中,所以应该尽量使它既容易记忆又相当不寻常。

#!/bin/sh

cat <<!FUNKY!
hello
this is a here
document
!FUNKY!

输出结果:

hello
this is a here
document

实验 here文档的另一个用法

我们从名为a_text_file的文件开始,它的内容如下所示:

That is line 1
That is line 2
That is line 3
That is line 4

你可以通过结合使用here文档和ed编辑器来编辑这个文件:

#!/bin/sh

ed a_text_file <<!FunkyStuff!
3
d
.,\$s/is/was/
w
q
!FunkyStuff!

exit 0

运行这个脚本程序,现在这个文件的内容是:

That is line 1
That is line 2
That was line 4

实验解析

这个脚本程序只是调用ed编辑器并向它传递命令,先让它移动到第三行,然后删除该行,再把当前行(因为第三行刚刚被删除了,所以当前行现在就是原来的最后一行,即第四行)中的is替换为was。完成这些操作的ed命令来自脚本程序中的here文档——在标记!FunkyStuff!之间的那些内容。

注意,我们在here文档中用\字符来防止$字符被shell扩展。\字符的作用是对$进行转义,让shell知道不要尝试把$s/is/was/扩展为它的值,而它也确实没有值。shell把\$传递为$,再由ed编辑器对它进行解释。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值