LinuxSir.cn,穿越时空的Linuxsir!

 找回密码
 注册
搜索
热搜: shell linux mysql
查看: 1313|回复: 12

Shell 程序设计:错误报告

[复制链接]
发表于 2005-5-15 13:56:22 | 显示全部楼层 |阅读模式
Shell 是一门高级语言,使用它可以方便地完成许多功能。但是,写 Shell 程序时一定要注意被调用的程序是在哪个 Shell 里执行:当前的 Shell 或者子 Shell。在极少数的情况下,这两种方式有着微妙的区别。我们用错误报告来说明这种差别。

对于一个 C 程序员来说,写出这样的代码是很自然的事情:
  1. #include <stdio.h>
  2. #include <stdarg.h>
  3. #include <errno.h>
  4. #include <string.h>
  5. #include <stdlib.h>

  6. void error(int n, char *fmt, ...)
  7. {
  8.     extern char *progname;
  9.     va_list v;

  10.     if (!progname)
  11.         fprintf(stderr, "%s: ", progname);
  12.     va_start(v, fmt);
  13.     vfprintf(stderr, fmt, v);
  14.     va_end(v);
  15.     if (errno)
  16.         fprintf(stderr, ": %s", strerror(errno));
  17.     fprintf(stderr, "\n");
  18.     exit(n);
  19. }
复制代码

这个函数集中处理了错误报告。我们可以在代码中调用它来完成 UNIX Shell 里的日常操作:
  1. char *progname = argv[0];
  2. char *datafile="somefile";
  3. /* ... ... */
  4. error(1, "cannot open file %s", datafile);
复制代码

如果要用 Shell 来写一个希望具有良好用户界面的程序,可能会需要类似的代码。同大多数情况一样,Shell 版本的程序比 C 版本的简单许多:
  1. die()
  2. {
  3.     local n=$1
  4.     shift
  5.     test ! -z $PROGNAME && echo -n "${PROGNAME}: " 1>&2
  6.     echo $* 1>&2
  7.     exit $n
  8. }
复制代码

通常,这种函数都会被写在一个单独的文件里,以便随时通过文件名来引用到这个函数。我们可以写一个简单的程序来验证它:
  1. $ ls
  2. environ  f
  3. $ cat environ
  4. # die(int n, string s1, string s2, ...)
  5. die()
  6. {
  7.         local n=$1
  8.         shift
  9.         test ! -z $PROGNAME && echo -n "${PROGNAME}: " 1>&2
  10.         echo "$*" 1>&2
  11.         exit $n
  12. }
  13. $ cat f
  14. #!/bin/sh

  15. . environ

  16. g()
  17. {
  18.         test -z $ABC && die 1 "ABC does not exist"
  19.         echo ABC exists
  20. }

  21. g
  22. $ ./f
  23. ABC does not exist
  24. $ ABC=aaa ./f
  25. ABC exists
  26. $
复制代码

这看上去很成功。但是,这样写是有问题的。考虑下面的代码:
  1. $ cat g
  2. #!/bin/sh

  3. . environ

  4. g()
  5. {
  6.         test -z $ABC && die 1 "ABC does not exist"
  7.         echo $ABC
  8. }

  9. COPY_OF_ABC=$(g)
  10. echo The value of ABC is: $COPY_OF_ABC
  11. $ ./g
  12. ABC does not exist
  13. The value of ABC is:
  14. $
复制代码

函数 g 的目的很明显:如果 ABC 存在,就打印 ABC 的值,这个值可以被其他的程序得到;否则,就 (向标准错误上) 输出 ABC does not exist 并且结束 Shell 程序的执行。$() 的作用与 `` 相同:用 $() 中程序执行的结果来代替 $() 本身。不过一些 Shell 会使用子 Shell 来执行 $() 或 `` 中的内容,在这样的 Shell 里 (如 Bash) 上面这样写是无效的,甚至有时连返回值也无法取到:
  1. $ cat g
  2. #!/bin/sh

  3. . environ

  4. g()
  5. {
  6.         test -z $ABC && die 1 "ABC does not exist"
  7.         echo $ABC $1
  8. }

  9. f()
  10. {
  11.         local COPY_OF_ABC=$(g)
  12.         echo $?
  13.         echo The value of ABC is: $COPY_OF_ABC
  14. }

  15. f
  16. $ ./g
  17. ABC does not exist
  18. 0
  19. The value of ABC is:
  20. $
复制代码

只有依靠临时文件避免使用 $() 或 `` 才能解决这个问题。不过这就需要做好善后工作,避免进程意外退出时留下垃圾。下面是一个例子。
  1. $ cat g
  2. #!/bin/sh

  3. . environ

  4. g()
  5. {
  6.         test -z $ABC && die 1 "ABC does not exist"
  7.         echo $ABC $1
  8. }

  9. f()
  10. {
  11.         local tmpfile="/tmp/f.$$"
  12.         trap "rm -f $tmpfile" 0 SIGINT SIGQUIT SIGKILL SIGTERM
  13.         g >$tmpfile
  14.         COPY_OF_ABC=$(cat $tmpfile)
  15.         echo The value of ABC is: $COPY_OF_ABC
  16.         rm -f $tmpfile
  17. }

  18. f
  19. $ ./g
  20. ABC does not exist
  21. $
复制代码

现在似乎可以了:调用 die 可以保证 Shell 程序的结束,而且 trap 的使用导致不会留下任何垃圾。但是这样做还是有问题的,因为如果有一连串的动作的话,仍然会留下垃圾。
  1. $ cat g
  2. #!/bin/sh

  3. . environ

  4. g()
  5. {
  6.         test -z $ABC && die 1 "ABC does not exist"
  7.         echo $ABC $1
  8. }

  9. f()
  10. {
  11.         local tmpfile="/tmp/f.$$"
  12.         trap "rm -f $tmpfile" 0 SIGINT SIGQUIT SIGKILL SIGTERM
  13.         g >$tmpfile
  14.         COPY_OF_ABC=$(cat $tmpfile)
  15.         echo $COPY_OF_ABC
  16.         rm -f $tmpfile
  17. }

  18. h()
  19. {
  20.         local tmpfile="/tmp/h.$$"
  21.         trap "rm -f $tmpfile" 0 SIGINT SIGQUIT SIGKILL SIGTERM
  22.         f >$tmpfile
  23.         local COPY_OF_COPY_OF_ABC=$(cat $tmpfile)
  24.         echo The value of COPY_OF_COPY_OF_ABC is: $COPY_OF_COPY_OF_ABC
  25.         rm -f $tmpfile
  26. }

  27. h
  28. $ ls /tmp

  29. $ ./g
  30. ABC does not exist
  31. $ ls /tmp
  32. h.7776
  33. $
复制代码

这是因为函数 h 创建了临时文件,而函数 f 只考虑删除自己的临时文件。因此,我们需要维护一个完整的临时文件的列表,并保证所有的垃圾在退出时都可以被清理掉。最后的版本是这样的:
  1. $ cat environ
  2. # die(int n, string s1, string s2, ...)
  3. die()
  4. {
  5.         local n=$1
  6.         shift
  7.         test ! -z $PROGNAME && echo -n "${PROGNAME}: " 1>&2
  8.         echo "$*" 1>&2
  9.         exit $n
  10. }

  11. # append(string env, string file)
  12. append()
  13. {
  14.         if eval test -z "\$$1" ; then
  15.                 eval $1=$2
  16.         else
  17.                 eval $1="\$$1 $2"
  18.         fi
  19. }

  20. # deappend(string env, string file)
  21. deappend()
  22. {
  23.         eval $1="$(eval echo \$$1 | awk '{ printf "%s", $1
  24.                 for (i = 2; i < NF; i++)
  25.                         printf " %s", $i
  26.                 if ($i != "'$2'")
  27.                         printf " %s", $i
  28.                 }')"
  29. }

  30. $ cat g
  31. #!/bin/sh

  32. . environ

  33. g()
  34. {
  35.         test -z $ABC && die 1 "ABC does not exist"
  36.         echo $ABC $1
  37. }

  38. f()
  39. {
  40.         local tmpfile="/tmp/f.$$"
  41.         append TMPFILE $tmpfile
  42.         trap "rm -f $TMPFILE" 0 SIGINT SIGQUIT SIGKILL SIGTERM
  43.         g >$tmpfile
  44.         COPY_OF_ABC=$(cat $tmpfile)
  45.         echo $COPY_OF_ABC
  46.         rm -f $tmpfile
  47.         deappend TMPFILE $tmpfile
  48. }

  49. h()
  50. {
  51.         local tmpfile="/tmp/h.$$"
  52.         append TMPFILE $tmpfile
  53.         trap "rm -f $TMPFILE" 0 SIGINT SIGQUIT SIGKILL SIGTERM
  54.         f >$tmpfile
  55.         local COPY_OF_COPY_OF_ABC=$(cat $tmpfile)
  56.         echo The value of COPY_OF_COPY_OF_ABC is: $COPY_OF_COPY_OF_ABC
  57.         rm -f $tmpfile
  58.         deappend TMPFILE $tmpfile
  59. }

  60. h
  61. $ ls /tmp

  62. $ ./g
  63. ABC does not exist
  64. $ ls /tmp

  65. $
复制代码

这里主要添加了 append 函数和 deappend 函数。append 函数的作用很简单,就是将一个值追加到一个环境变量的最后;deappend 则用来去掉一个环境变量最后的字符串,但前提是最后的字符串与要去掉的相同。这样,就可以保证无论在什么情况下都能即时地从 Shell 程序里退出并且不留垃圾了。

最后要说明的是,虽然上面的程序看起来是无用的,但是在实际情况中经常会遇到这样的代码,它们只是形式不同,实质上是一样的。
发表于 2005-5-15 14:02:11 | 显示全部楼层
好累.看不懂.   
回复 支持 反对

使用道具 举报

发表于 2005-5-15 14:10:56 | 显示全部楼层
Post by Freebird
好累.看不懂.   

me2!
回复 支持 反对

使用道具 举报

 楼主| 发表于 2005-5-15 14:10:59 | 显示全部楼层
Post by Freebird
好累.看不懂.   

5555~~~~
您怎么老对我说这样的话呢?我还记得上一次,您说 “Shell 脚本写成这样真是不敢恭维”。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2005-5-15 14:19:02 | 显示全部楼层
摘要

用 C 写程序,可以用 exit 随时结束程序的执行。用 Perl,可以用 die。用 Shell 的什么呢?exit?这是我最初的想法。可是当我准备应用它时,我发现有些微妙的错误,就像上面提到的那样,有时 exit 无法成功地完成我们寄予它的希望。当我解决了这个问题时,我就把心得写了出来。
回复 支持 反对

使用道具 举报

发表于 2005-5-15 17:06:31 | 显示全部楼层
我通读了一遍文章
herberteuler兄所以写这样的错误报告的文章是否为了调试?

若是调试,我在脚本某处echo就成了.另外也听说有特别的脚本的调试工具,不过我没用过.别人也许有更特别的方法,但专门写一个无通用性的函数来错误报告似乎不可取.
你能说明一个我无法拒绝的理由吗?
回复 支持 反对

使用道具 举报

 楼主| 发表于 2005-5-15 17:14:37 | 显示全部楼层
我现在要做一个程序,除非特别,只能用 POSIX Shell 来写;此外,它的行为要与完成同样功能的、用 C 或者 Perl 或者 Python 写的程序并无二致。你可以这样想像,就是 rpm 或者 apt 或者 emerge 的用 Shell 实现的版本。对于这种程序来说,某处的执行失败必须导致程序的结束并且打印出有用的提示信息。但是很快我就发现 exit 并不能保证在任何情况下退出 Shell 程序,这里的程序就是例子。无可奈何之下,只能用这个方法了。

btw,1. 为什么说这是一个无通用性的函数呢?
2. 调试的话,我觉得 set -vx 就很好了。
回复 支持 反对

使用道具 举报

发表于 2005-5-15 17:29:39 | 显示全部楼层
Post by herberteuler
5555~~~~
您怎么老对我说这样的话呢?我还记得上一次,您说 “Shell 脚本写成这样真是不敢恭维”。


只怪偶水平有限   
回复 支持 反对

使用道具 举报

 楼主| 发表于 2005-5-15 18:19:47 | 显示全部楼层
也许是我的程序写得有问题。我把它放上来,欢迎提出建议。这样执行:首先,把文件解压:
  1. $ tar xzf dipmt.tar.gz
  2. $ cd dipmt/usr/lib/dipmt/lib/
  3. $ make
  4. $ PKGASSISTDIR=../../../../pkgassist ./pkgop xterm
  5. $ PKGASSISTDIR=../../../../pkgassist ./pkgop aterm
  6. $ PKGASSISTDIR=../../../../pkgassist ./pkgop dev-libs/aterm
复制代码

程序还没有开发完成,所以比较麻烦,请原谅。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x
回复 支持 反对

使用道具 举报

发表于 2005-5-15 23:49:03 | 显示全部楼层
Post by herberteuler
但是很快我就发现 exit 并不能保证在任何情况下退出 Shell 程序,这里的程序就是例子。无可奈何之下,只能用这个方法了。

上面的die函数的使用:
f 中避免了用子shell来确保exit能从shell脚本中退出
那....
我要是在f中避免了用子shell,那也能确保exit能使脚本退出.
另外,若是为了使用die函数而使你的bash脚本少用了子shell,那岂不是把shell的功能减了一些?我认为得不偿失

Post by herberteuler

btw,1. 为什么说这是一个无通用性的函数呢?

因为不能用它来开发里面用了子shell的函数

或许我的理解有误,还请herberteuler兄继续指正
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表