eval命令

eval其格式如下:```sh
eval command-line

  1. 其中,`command-line`是可以在终端中输入的普通命令行。如果你把`eval`放在命令行之前,`Shell`会对其进行二次扫描,然后执行。如果你使用脚本构造出的命令需要被调用,那么`eval`的这个功能就非常有用了。
  2. 例如:
  3. ```sh
  4. $ pipe="|"
  5. $ ls $pipe wc -l
  6. |: No such file or directory
  7. wc: No such file or directory
  8. -1: No such file or directory

ls命令发出错误的原因在于pipe的值以及随后的wc -l都被视为命令参数。Shell是在变量替换之前处理管道和I/O重定向的,因此不可能再去解释pipe中保存的管道符号。把eval放在命令序列前面就能够得到想要的结果:

  1. $ eval ls $pipe wc l
  2. 16

Shell第一次扫描命令行时,它会将pipe替换成对应的值|。然后eval会使得Shell重新扫描命令行,这时候Shell识别出了作为管道符号的|,接下来的事情就顺理成章了。
Shell程序中,eval常用于从变量中构造命令行。如果变量中包含了任何必须由Shell解释的字符,那就必须用到eval。命令终止符(;|&)、I/O重定向(<>)以及引号都属于这类字符,必须直接出现在命令行上,只有这样,Shell才能识别出它们的特殊含义。
以下程序的目的是显示出传入的最后一个参数。

  1. $ cat last
  2. eval echo \$$#
  3. $ last one two three four
  4. four
  5. $ last * # 得到最后一个文件
  6. zoo_report

Shell第一次扫描echo \$$#时,反斜线指明忽略其后的$。然后Shell碰到了特殊参数$#,因此将其替换为命令行上对应的值。这条命令现在就变成了下面的样子:echo $4在第一次扫描完毕后,Shell就删除了反斜线。当第二次扫描该命令行时,Shell$4替换成对应的值,然后执行echo

wait命令

如果你将某个命令移入后台,那么该命令会在一个独立于当前Shell的子Shell中运行(这称为异步执行)。有时候,你可能希望等待后台进程(也称为子进程,因为它是由当前Shell,也就是父Shell生成的)执行完之后再继续往下处理。例如,你将一个需要对大量数据排序的sort移入后台,在能够访问排序过的数据之前,只能等待后台进程完成。
wait命令可以满足这种需求。其一般格式为:

  1. wait process-id

其中,process-id是要完成的进程的PID。如果忽略这个参数,Shell会等待所有的子进程执行完毕。在等待进程执行完之前,当前Shell会被挂起。

$!变量

如果你只有一个后台进程,那么不带参数的wait命令就够了。但如果在后台运行的命令不止一个,你想等待最近的那个,可以使用特殊变量$!作为最近那个后台命令的进程ID。因此,下列命令:

  1. wait $!

会等待最近移入后台的进程执行完毕。配合一些中间变量,就可以将这些进程ID保留起来以供后面使用:

  1. prog1 &
  2. pid1=$!
  3. ...
  4. prog2 &
  5. pid2=$!
  6. ...
  7. wait $pid1 # 等待 prog1 结束
  8. ...
  9. wait $pid2 # 等待 prog2 结束

trap命令

如果你在Shell程序执行过程中在终端上按下DELETE或BREAK键,程序通常会被终止,并提示你输入下一条命令。这种方式对于Shell 程序而言未必总是可取的。因为这有可能会给你留下一堆临时文件,而这些临时文件在程序正常结束的情况下都是会被清理掉的。
Shell程序中的信号处理是通过trap命令实现的,其一般格式为:

  1. trap commands signals

其中,commands是接收到由signals指定的信号时要执行的一个或多个命令。不同类型的信号都有各自的助记名和编号。

信号 助记名 产生原因
0 EXIT 退出 Shell
1 HUP 挂起
2 INT 中断(如按下了 DELETE 或 Ctrl+c 键)
15 TERM 软件终止信号(默认由 kill 命令发送)

下面的例子演示了当用户尝试在终端上终止程序的时候,如何使用trap命令清除文件后再退出: sh trap "rm $WORKDIR/work1$$ $WORKDIR/dataout; exit" INT 执行trap之后,当程序接收到SIGINT(信号编号为2),文件work1dataout$$就会被自动删除。如果随后用户终止了程序执行,可以确保这两个临时文件不会留在文件系统中。rm后面的exit是必需的,如果漏写的话,程序会一直停留在接收到信号时的执行位置上。 在出现挂起的时候会产生编号为1的信号(SIGHUPHUP):这个信号最初和拨号连接有关,不过现在更多指的是连接意外中断(如 Internet 连接掉线)。你可以修改之前的trap命令,将SIGINT也加入到信号列表中,这样在出现SIGHUP信号时也能够删除指定的那两个文件: sh trap "rm $WORKDIR/work1$$ $WORKDIR/dataout; exit'' INT HUPtrap指定的命令序列(也称为 trap 处理程序)如果不止一个命令,则必须将其全部放入引号中。另外要注意,Shell会在执行trap时扫描命令行,在接收到信号列表中的信号时还会再扫描一次。 在上面的例子中,WORKDIR会在执行trap命令时被替换。如果你希望替换发生在接收到信号时,可以把这些命令放在单引号中: sh trap 'rm $WORKDIR/work1$$ $WORKDIR/dataout$$; exit' INT HUP

不使用参数的trap

如果在执行trap时不带参数,会显示出你定义过或修改过的所有trap处理程序:

  1. $ trap 'echo logged off at $(date) >>$HOME/logoffs' EXIT
  2. $ trap 列出修改过的 trap 处理程序
  3. trap 'echo logged off at $(date) >>$HOME/logoffs' EXIT
  4. $ Ctrl+d 登出
  5. login:steve 重新登入
  6. Password:
  7. $ cat $HOME/logoffs 查看结果
  8. logged off at Wed Oct 2 15:11:58 EDT 2002

设置好的trap处理程序会在Shell接收到退出信号(信号 0,即EXIT)时执行。因为是在登录Shell中做出的设置,所以当你登出的时候,trap处理程序会将你登出的时间写入文件$HOME/logoffs。将命令放入单引号中是为了避免定义trap处理程序时执行date。不带参数的trap命令列出了针对信号0(EXIT)要采取的新处理方法。当steve登出系统,然后又登入系统后,从文件$HOME/logoffs中可以看出已经执行了echo命令,trap处理程序运行正常。

忽略信号

如果trap中没有列出命令,那么指定的信号会被忽略。例如,下列命令:

  1. trap "" SIGINT

指定忽略中断信号。在执行某些不希望被打断的操作时,你可能需要去忽略某些信号。
注意,在指定信号时,trap 允许使用信号编号、信号简称(INT)或者信号全称(SIGINT)。
如果你忽略了某个信号,所有的子Shell也会忽略这个信号。如果你指定了某个信号的处理程序,当所有的子Shell接收到该信号时,自动采用默认的处理方式,而不是新指定的处理程序。

重置信号

如果你修改了信号的默认处理方式,还可以将其再改回原样,只需要忽略trap的第一个参数就行了:

  1. trap HUP INT

该命令会重置SIGHUPSIGINT信号的处理方式。
很多Shell程序中还是用了这种写法:

  1. trap "/bin/rm -f $tempfile; exit" INT QUIT EXIT

它可以确保如果在退出时临时文件还没有创建的话,rm命令不会产生错误信息。如果临时文件存在,trap处理程序会将其删除;如果不存在,则什么都不做。

I/O的高级

你已经知道<>>>分别对应着输入重定向、输出重定向以及采用追加方式的输出重定向。另外也应该知道可以在命令行上使用2>来实现标准错误重定向:

  1. command 2> file

有时候你可能希望在程序中明确地向标准错误中写入。只需要对上面的写法稍作改动,就可以将标准输出重定向到标准错误:

  1. command >&2

>&指明将输出重定向到与指定的文件描述符相关联的文件中。文件描述符0对应标准输入,描述符1对应标准输出,描述符2对应标准错误。一定要记住的是>&之间绝不能有空格。
你也许想将程序的标准输出(通常简称 stdout)和标准错误(stderr)都重定向到同一个文件中。如果知道文件名,那么操作方法很直接:

  1. command > foo 2>> foo

你也可以写作:

  1. command > foo 2>&1

<&->&-

>&-能够关闭标准输出。如果它出现在文件描述符之后,则会关闭与之关联的文件。因此:

  1. ls >&-

<&-能够关闭标准输入。

行内输入重定向

如果<<出现在命令之后:

  1. command <<word

Shell会使用之后的行作为命令输入,直到碰上只包含word的行。下面是一个简单的例子:

  1. $ wc -l <<ENDOFDATA 使用 ENDOFDATA 之前的行作为标准输入
  2. > here's a line
  3. > and another
  4. > and yet another
  5. > ENDOFDATA
  6. 3

Shell会将每一行都作为wc的标准输入,一直到只包含ENDOFDATA的那一行为止。
行内输入重定向是一项非常有用的特性。它可以让你在程序中直接指定命令的标准输入,避免了还得使用其他的输入文件的麻烦,或是通过echo将输入内容送入命令的标准输入。下面是Shell程序中利用该特性的一个常见例子:

  1. mail $* <<END-OF-DATA
  2. Attention:
  3. Our monthly computer users group meeting
  4. will take place on Friday, March 4, 2016 at
  5. 8pm in Room 1A-308. Please try to attend.
  6. END-OF-DATA

要想把这则消息发送给保存在文件users_list中的所有组员,可以这么做:

  1. mailmsg $(cat users_list)

Shell会对重定向的输入数据进行参数替换、执行反引号中的命令,还能够识别出现的反斜线字符。
Shell会解释其中的美元符号反引号反斜线。如果你希望所有的输入行原封不动,在<<后的单词前加上一个反斜线。

  1. $ cat <<FOOBAR
  2. > $HOME
  3. > *****
  4. > \$foobar
  5. > `date`
  6. > FOOBAR # 终止输入
  7. /users/steve
  8. *****
  9. $foobar
  10. Wed Oct 2 15:23:15 EDT 2002

Shell会将FOOBAR之前的所有行作为cat的输入,因此会替换掉其中的HOME,由于foobar前面有反斜线,故不会替换该变量。执行 date命令的原因在于Shell会执行反引号中的命令。
大多数现代 Shell 还理解另一种写法:如果<<后面的第一个字符是连接符(-),那么输入中的前导制表符都会被Shell删除。如果希望通过可视化缩进来提高重定向文本的可读性,但同时仍希望按照正常的左对齐形式输出,可以利用这个特性来实现:

  1. $ cat <<-END
  2. > Indented lines
  3. > because tabs are cool
  4. > END
  5. Indented lines
  6. because tabs are cool

函数

所有的现代Shell都支持函数:或长或短的命令序列都可以在Shell程序中根据需要被引用或重用。定义函数的一般格式为:

  1. name () { command; ... command; }

其中,name是函数名,小括号表示这是一个函数定义,花括号中的命令定义了函数体。这些命令会在函数被调用时执行。
注意,如果函数体和花括号都出现在同一行中,{和第一条命令之间必须至少有一个空白字符,最后一条命令和}之间必须有一个分号。
下面定义了一个名为nu的函数,可以显示出登录用户的数量:

  1. nu () { who | wc -l; }

调用函数就像执行普通命令一样,在Shell中输入函数名就行了:

  1. $ nu
  2. 22

函数对于Shell程序员非常有用,它能够避免开发过程很多乏味的重复输入。函数有一个重要的特性:命令行上出现在函数后的参数会被依次分配给位置参数$1$2……,就像其他命令一样。

  1. $ nrrun () { tbl $1 | nroff -mm -Tlp | lp; }
  2. $ nrrun memo1 memo1 上运行函数
  3. request id is laser1-33 (standard input)

函数仅存在于所定义它的Shell中,无法传给子Shell。因为函数是在当前Shell中执行,所以对于当前目录或变量做出的修改在函数执行完毕之后依然会保留,就像是使用之前讲过的.命令调用函数一样:

  1. $ db () {
  2. > PATH=$PATH:/uxn2/data
  3. > PS1=DB:
  4. > cd /uxn2/data
  5. > }
  6. $ db 执行函数
  7. DB:

函数定义可以根据需要占据多行。Shell会通过辅命令行提示符提示继续输入函数体中的命令,直到使用}结束函数定义。
可以将常用函数的定义放进.profile中,这样无论你什么时候登录,都可以直接使用这些函数。或者是将所有的函数定义都放在一个文件中,如myfuncs,然后在当前Shell 中执行该文件:. myfuncs ,你现在应该知道,这会使得当前Shell能够使用myfuncs中所定义的全部函数。

删除函数

使用带有-f选项的unset命令可以从Shell中删除函数。

  1. $ unset f nu
  2. $ nu
  3. sh: nu: not found

return命令

如果你在函数内部使用exit,不仅会终止函数的执行,而且还会使调用该函数的Shell程序退出,返回到命令行。如果你只是想退出函数,可以使用return命令,其格式如下:

  1. return n

n作为该函数的返回状态。如果忽略的话,则使用函数中最后执行的那条命令的退出状态,这种情况也适用于函数中没有包含return语句的时候。返回状态在其他方面和退出状态一样:你可以使用Shell变量$?来访问它,也可以在ifwhileuntil命令中对其进行测试。

type命令

当你输入命令名执行命令时,如果知道这个命令是函数、Shell 内建函数、标准UNIX命令或者是 Shell 别名,那么还是有帮助的。这正是 type命令发挥作用的地方。type命令接受一个或多个命令名作为参数,可以告诉你这些命令是什么类型。下面是几个例子:

  1. $ nu () { who | wc -l; }
  2. $ type pwd
  3. pwd is a Shell builtin
  4. $ type ls
  5. ls is aliased to `/bin/ls -F'
  6. $ type cat
  7. cat is /bin/cat
  8. $ type nu
  9. nu is a function