用 scheme 语言进行 UNIX 系统编程

在这篇文章中,我们首先来看一看用 scheme 语言进行系统程序设计的一些基本的特征。在后续的文章中,我们将要看到更多的、更有用的 scheme 语言的系统程序设计的例子。

简介

图形用户界面的程序设计,以及关于万维网的程序设计,并不一定都是困难的、复杂的、让人头疼的、让人望而生畏的。如果有好的程序语言(scheme 语言),好的图形界面工具(zenity),好的万维网工具(curl),再加上好的程序构思(UNIX 的软件哲学),一些简单的任务就确确实实可以是容易的、简单的、让自己微笑的、让用户也开心的。在这篇文章中,我们首先来看一看用 scheme 语言进行系统程序设计的一些基本的特征。在后续的文章中,我们将要看到更多的、更有用的 scheme 语言的系统程序设计的例子。


小巧而强大的工具:scsh 和 zenity

scsh 是一个 scheme 语言的 UNIX shell 编程环境。我们知道,作为一个计算机操作系统来讲的 UNIX 的基本概念,就是在 kernel 上外加上 shell 来控制这个计算机的方方面面的操作。这个操作按照 shell 语言的语法和语义来进行。如果 shell 语言由于各种各样的原因,不能满足用户的操作需求,一般来说,用户就会求助于 C 语言和直接的 kernel 的 C 语言编程接口。从这个角度来看的话,shell 确实是这个系统里面非常关键的一环。

但是我们的使用经验也的确告诉我们,传统的 UNIX shell 并不是一个非常能令人感到舒适的用户界面环境,它有诸多的古怪之处。无论是传统 UNIX shell 的语法,还是它的语义,都不能让用户感到能够挥洒自如。新用户感到头疼,老用户也感到头疼。一方面,无论何时在需要使用一些高级一点的 shell 功能的时候,传统 shell 的语法就立刻变的让人难以忍受;另一方面,当自己的程序稍微变的规模大一点点的时候,自己看着自己写的这堆 shell 代码,真是怎么看怎么让自己难受,更不用说如果别人看到的话会怎么样了。

而 scsh 恰恰就能解决传统 UNIX shell 的这两个毛病。这当然首先要感谢 scheme 语言的精巧了。scsh 成功的把 scheme 语言的精致和灵巧融入在了 UNIX shell 的环境里面;或者不如反过来说,scsh 成功的把鲁莽而强大的 UNIX 系统,驯服在了 scheme 语言提供的优雅有力而且准确的环境里面。关于这些的证明,我们在下面的多个例子里面很快就能一一看到。

本文由于篇幅的限制,不可能详细的介绍 scheme 语言。但是我们尽量的在讲到例子的时候, 把相关的 scheme 语言的内容都讲到。不过我们还是建议读者朋友们在阅读本文以前,能够 事先对 scheme 语言有一定的了解。在本文的"参考资料"部分列有关于 scheme 语言的一 些网上资料的链接。

zenity 是一个非常简单的 GTK+ 2 的对话框工具。它可以在 UNIX shell 环境下,经由标 准的 shell 命令的方式,弹出 GTK+ 2 的对话框。这就使得在 UNIX shell 环境下编写一 些利用到这样的图形用户界面的对话框功能的程序,变的十分的容易。一个简单的 shell 命令就可以了。当然,目前的 zenity 还仅仅只是一个十分简单的对话框工具。只能显示出 种类有限的一些 GTK+ 2 的对话框,而且可以配置的地方也还不够多。不过,就本文的目的 来说,zenity 眼前提供的功能已经足够了。在本文的结语部分,关于 zenity,我们还有另 一些话要说。


定时提醒自己,该让眼睛休息了

我们首先要介绍的一个例子,是这样的一个用 scsh 和 zenity 来完成的一个小工具:当用 户连续坐在计算机屏幕前面工作超过一个小时以后,就会强行弹出一个对话框,劝告用户该 休息了,要注意保护眼睛,然后就强行起动屏幕保护程序,以迫使用户离开计算机屏幕,让 眼睛得到休息。

首先要说明的是,我们并不打算为这件事情而大动干戈,用 C 语言写上一个几千行代码的 程序,弄一个"完美"的解决方案出来。恰恰相反,我们要寻找的是一个简单的解决办法, 能够越简单越好。所以我们希望能够尽量的利用系统上已有的工具,然后用 UNIX shell 的 办法把这些工具组合起来。但是和传统的 UNIX shell 编程不同的是,我们这里要用到的不 是传统的 bash 或者 zsh 或者其它类似的、经典的 UNIX shell 程序;我们将要用到的是 scsh 这个 scheme 语言的 UNIX shell。我们要由这个例子,初步的了解一下 scsh 的一些 基本的特征。并且,这个小程序也的确是个有用而且很有意思的小程序。

首先介绍一下 xscreensaver-command 这个程序。这是一个用来控制 xscreensaver 这个 X Window 窗口系统的屏幕保护程序的一个小程序。这个由 Jamie Zawinski 维护的 X 窗口系 统的屏幕保护程序,它的工作原理是这样的:后台有一个进程负责监听来自鼠标和键盘的事 件;如果有一段时间没有鼠标或者键盘事件的话,这就说明用户已经离开计算机了,这时候 就可以起动一个画图程序来作为屏幕保护;一旦后台的进程重新监听到来自鼠标或者键盘的 事件,这就说明用户又回到了计算机屏幕前面,这时候,就会把画图程序终止;这样屏幕保 护就被撤销,用户又可以重新使用计算机了。

而 xscreensaver-command 这个命令是干什么的呢?它可以主动的和 xscreensaver 的后台 进程对话;把用户从 xscreensaver-command 的命令行上传递过来的命令,翻译了,然后再 传递给 xscreensaver 的后台进程。这样一来就可以从 shell 命令行上直接控制后台进程 做到一些用户要求的事情:比如不去管用户的鼠标或者键盘的事件,而立刻激活屏幕保护; 又或者是查询当前屏幕保护程序是否正在运行,或者上一次运行到现在已经经过了多长时间; 等等。xscreensaver-command 有两个命令行上的开关特别值得我们的注意。一个是 -activate 这个开关,这可以强迫屏幕保护立即起动;另一个是 -watch 这个开关,这个可 以查询屏幕保护程序的当前运行状态。

好了,我们的眼睛保护程序(中文名字叫做"卿卿","卿卿"很关心你的眼睛健康的哦!) 就依赖于这个屏幕保护程序。基本的思路,就是在屏幕保护程序的帮助下,了解用户的眼睛 是否在工作,当用户的眼睛工作了一定长的时间以后,眼睛保护程序就强行起动屏幕保护程 序,迫使用户离开计算机,这样以达到保护眼睛的目的。这是一个不到一百行的 scsh 小程 序。完整的程序文本的下载链接,在本文结尾的部份列出。下面,我们就来仔细的分析一下 这个程序,主要的目的,是要经由这个分析,了解一下 scsh 和 zenity 的一些基本的特征。


程序分析

我们的程序就是一个单个的 scsh 脚本文件。连注释一起算在内,全部不到一百行的一个小 程序,普通的文本文件。

#! /usr/bin/scsh \
-o locks -o threads -e start-eys-tender -s
!#

这是我们的 scsh 脚本程序的文件开头。可以看出来,这是和传统的 UNIX shell 脚本程序 的文件开头很相似的。也是用一个"#!"在最前面,然后是 scsh 的执行路径。对于 scsh 来说,"#!"和"!#"起到一个注释的作用。这个文件开头在 UNIX 系统的 loader 看来, 是说明了一个程序解释器的路径,以及要交给程序解释器的命令行开关;等到系统 loader 把 scsh 装载起来,并且把这个脚本文件喂给 scsh 以后,scsh 看到的这个用"#!"和"! #"包围起来的这一段文字,就被认为是 scsh 脚本程序的注释。这些都是标准的 UNIX 系 统处理脚本程序的技术的小小变化。

上面传递给 scsh 解释器的几个命令行开关,-o 表示打开一个 scsh 模块(module),我 们在上面打开了 locks 和 threads 两个模块,这两个模块是关于多线程编程的;开关 -e 表示程序的入口点,我们在这里指定我们这个脚本程序的入口点为 start-eyes-tender 这 个函数,我们在下面将会看到这个 start-eyes-tender 函数的定义;和 C 语言不同的是, scsh 脚本的入口函数的名称是可以随便按照我们的要求而定义的;和传统 UNIX shell 不 同的是,传统 shell 是不能定义入口函数的,shell 脚本会按顺序一行一行的执行下来, 而我们这里的 scsh 脚本则可以定义入口函数,这对于程序的测试和编写,都是有一定帮助 的;最后说明一下最后的这个 -s 这个命令行开关,它告诉 scsh 装载当前的脚本,进入脚 本解释的状态,而不要进入一个交互式环境。

关于 start-eyes-tender 这个函数的定义:

(define start-eyes-tender
  (lambda ignore
    (watch-screensaver)))

这是 scheme 语言中定义函数的一种写法,关于这方面的内容,我们下面还会讲到。在这里 需要指出的是,这里的这样一个函数的定义的写法,它有一个特别的好处:这样的定义的写 法,使得我们可以用随便多少个参数去调用这个函数,也可以完全不要参数就调用这个函数。 在上面的这个具体的例子当中,所有的参数,如果有的话,都被忽略了。我们也可以根据具 体的使用情况,不忽略这些参数。我们以后会看到这样的例子。这是这样的写法的第一个好 处。这样的写法的另一个好处,是我们可以在 watch-screensaver 这个函数被定义之前, 就使用到这个函数。这给我们书写程序带来了很大的方便。不像在其它的一些更为初级的语 言中,在使用一个函数之前,必须先有一个 prototype 的声明,或者要把使用到的函数的 定义放在文件的前面一部分才行。这样的一些令人不舒服的限制,使得我们在这些比较低级 的语言当中,比如 C 语言当中,都有这样的经历:就是我们在阅读别人的程序代码的时候, 往往要从文件的结尾部分开始读起,要把别人源代码文件倒过来看。

顺便说一下,正是部分的由于要克服这个令人非常不舒服"倒过来读"的限制,伟大的计算 机科学家 Donald E. Knuth 才发明了 Literate Programming 这个设计,以及相应的 WEB 和 CWEB 这两套分别适用于 Pascal 语言和 C 语言的 Literate Programming 工具。注意 这里说的这个 WEB 和 World Wide Web 可没有什么特别的关系的哦。8)


常量声明

下面进入 scsh 的 scheme 语言的程序的部分。

(define eyes-work-interval 3600)

scheme 语言的程序,简单说来,是由一个接一个的 sexp 组成。所谓 sexp,就是 symbol expression 的缩写,意思是"符号表达式"。这些 sexp 一般都是一对圆括弧括起来的一 系列的字符串,当中用空格划分开来。一般的情况下,由其中的第一个字符串,来负责对这 一对圆括弧括起来的里边的内容进行解释。上面的这个 (define ...) 就是定义了一个常量, 常量的名称为 eyes-work-interval,常量的值为 3600。我们注意到 scheme 语言不需要程 序员直接声明常量的数据类型,scheme 的动态类型系统会负责处理这些事情。这样就省去 了程序员不少的麻烦。我们这里定义的这个常量,意思是,在用户的眼睛连续工作 3600 秒, 也就是一个小时以后,眼睛保护程序就会强制起动屏幕保护程序。用户可以根据自己的实际 需要,改变这个常量的值,比如说改成 45 分钟,也就是 45 * 60 秒:

(define eyes-work-interval (* 45 60))

我们注意到,在 scheme 语言里面,按照 sexp 的精神,我们把 45 * 60 写成了 (* 45 60) 这样的格式。由于篇幅的关系,我们不可能对 scheme 语言做详细的介绍,对于 scheme 这样的语法的好处,我们以后有机会,再详细的论述。接着往下看我们的程序。


多线程的同步

下面的这三个函数可以帮助在两个线程之间进行一些简单的同步的工作。

(define (make-locker val)
  (cons (make-lock) val))
(define (locker-set! locker val)
  (let ((lock (car locker)))
    (obtain-lock lock)
    (set-cdr! locker val)
    (release-lock lock)))
(define (locker-value locker)
  (let ((lock (car locker)))
    (obtain-lock lock)
    (let ((val (cdr locker)))
      (release-lock lock)
      val)))

这里定义的三个函数,这是在 scsh 的 locks 模块上稍微做了一点点的润色。首先我们注 意到 scheme 语言定义函数的语法,和前面提到的定义常量的语法乍看上去很相似,这里面 的区别在于,定义函数是:

(define (fun arg1 arg2 arg3) ...)

这是一个接收三个参数的函数。如果是一个不接收参数的函数,这样的函数在 scheme 的 "行话"里面被叫做"thunk",那么它的定义是像下面这个样子的:

(define (joy) ...)

至于定义一个常量则是下面这个样子:

(define var ...)

上面定义的这三个函数,其中,make-locker 函数被用来在程序中声明一个 semaphore 结 构。这是由 scsh 的 locks 模块中的 make-lock 函数运行的结果,这就是得到了一个 lock 类型的数据结构,然后再加上一个变量而得到我们的 semaphore 的。一个 lock 类型 的数据结构,经由 obtain-lock 和 release-lock 两个函数,就可以完成两个线程间的简 单的同步,而我们又给它加上了一个变量,这样就可以在两个线程同步的情况下,传递这个 变量的值了。我们用 locker-set! 来设置这个 semaphore 的值;用 locker-value 来获取 这个值。在这里再说明一下 scheme 语言的函数调用的语法:如果一个函数的名称是 fun 的话,(fun arg1 arg2) 就表示用 arg1 和 arg2 作为参数,调用这个函数。如果还有另一 个函数的名称是 joy 的话,(joy argA (fun arg1 arg2) argB) 就表示用三个参数,分别 是第一个参数 argA;以及第二个参数,这是用 arg1 和 arg2 作为参数调用函数 fun 而得 到的结果;还有第三个参数 argB,这样一共三个参数去调用函数 joy。这样我们就看到了 scheme 语言中的嵌讨的函数调用的语法是怎样的了。这是一个很自然的,和 sexp 的思路 很合拍的语法。


闹钟函数

下面这个辅助函数起到一个定时器的作用,可以在等待一段时间以后,就执行一项事先计划 好的任务。在执行任务之前,还可以做一些检查。

(define (work/check-delay work check delay)
  (let ((thread (lambda ()
		 		   (sleep (* delay 1000))
		 		   (if (check)
		 		       (work)))))
    (spawn thread)))

这是一个辅助函数。我们看到,在 scheme 语言里面,在函数名中可以使用"/"和"-"这 样的字符。这个辅助函数是干什么用的呢?首先,它会分离出一个线程,这个是由 (spawn thread) 这个 sexp 来完成的。这个线程是由一个匿名的 lambda 函数来定义的;关于这样 的匿名的 lambda 函数,在 python 和 java 语言当中都有相类似的东西;这里的 lambda 这个单词来源于数理逻辑宗师 Alonzo Church 的关于 lambda 演算的理论;Church 是 Alan Turing 在普林斯顿的时候的老师;其实不仅仅是图灵,后来还有若干个获得图灵奖和 没有获得图灵奖的宗师级别的大学问家,他们的老师也都是 Church。

我们这里定义的这个 lambda 函数,它会先让自己的这个线程沉睡上一个指定的时间,等到 醒过来以后,就会先运行 check 函数,如果 check 得到的结果为真,就执行 work 函数, 然后再退出自己这个线程;如果 check 得到的结果为假的话,直接就结束当前线程了。在 这里我们看到这个 if 表达式的语法是 (if test clause1 clause2) 这样的,如果 test 为真的话,就执行 clause1,否则的话,就执行 clause2,这样得到的运算结果,作为 if 表达式的值。如果没有 clause2 的话,那么在 test 为假的情况下,这个 if sexp 的值就 是不确定的。我们这里的这个 if 的用法,我们并不关心 if 表达式的返回值,我们关心的, 是要在 (check) 成功的情况下,要执行 (work),否则就不要 (work)。有了这么样的一个 辅助函数,我们就可以指定一些任务,可以让这些任务在一定时间以后,如果有一定的条件 能得到满足的话,就可以执行这些任务指定的工作。这就是函数名 work/check-delay 的由 来。这个函数就有一点像一个比较智能化一点的闹钟一样,可以定时做一些事情,并且在做 事情以前,还可以设置一些检查的条件。


驱动函数

下面这个函数负责这个程序的整个的事件驱动。它做的事情基本上就是,等时间一到,就起 动 eyes-tender 函数,在这个 eyes-tender 函数里面将要做一些事情,以能够使得计算机 用户达到保护眼睛健康的目的。下面我们先来看这个 watch-screensaver 函数的全文。这 个函数之所以起了这么个名字,这是因为它的事件驱动是依靠观察 xscreensaver 这个屏幕 保护程序的状态来进行的。因为,在一定程度上,屏幕保护程序是否在运行,就大体上相当 于,计算机用户是否在使用眼睛,眼睛是否在一直工作,是否眼睛需要休息了。下面我们先 来看这个函数:

(define (watch-screensaver)
  (let ((blankq (list)))
    (let ((port (run/port (xscreensaver-command -watch)))
       (blankf (lambda ()
              (for-each (lambda (f) (f)) blankq)
              (set! blankq (list))))
       (unblankf
        (lambda ()
          (let ((locker (make-locker #t)))
            (let ((uncheck (lambda () (locker-set! locker #f)))
               (check (lambda () (locker-value locker))))
           (set! blankq (cons uncheck blankq))
           (work/check-delay eyes-tender check eyes-work-interval))))))
      (unblankf)
      (awk (read-line port) (line) ()
        ((: bos "BLANK") (blankf))
        ((: bos "UNBLANK") (unblankf))))))

这是整个系统的驱动函数。这里面有两个部分特别值得注意。一个是开头部分的 (run/port ...) 这个 sexp;还有一个是结尾部分的 (awk ...) 这个 sexp。我们将要一部分接一部分 的来分析。首先说明一下这个函数的大概意思。

这个函数调用 shell 命令 xscreensaver-command,用上了 -watch 开关。前面说过,这样 的话,这个 shell 命令就可以由 xscreensaver 的后台程序查询到当前屏幕保护程序的运 行状态。相关的细节,在 man xscreensaver-command 的手册页面里面有详细的说明。这样 一来,我们就能得到当前屏幕保护程序的运行状态;当屏幕保护程序停止运行的时候,我们 就会由监听端口得到一个 UNBLANK 字符串,这就说明用户正在开始在电脑屏幕前工作,那 么我们就要用前面说明的 work/check-delay 函数,在一个小时以后,如果用户的确是持续 的在计算机屏幕前工作了一个小时的话,那么就要起动屏幕保护,强迫用户离开电脑屏幕, 以让眼睛得到休息。而如果用户在还没有到一个小时的时候就早已离开了电脑屏幕,也就是 说,屏幕保护程序因为监听不到用户的鼠标或者键盘操作,而提前起动了的话,我们就会从 监听端口得到一个 BLANK 字符串,那我们就还要记得把先前预定的起动屏幕保护的计划给 撤销掉。这个机制是由下面的程序片段实现的:

(let ((locker (make-locker #t)))
  (let ((uncheck (lambda () (locker-set! locker #f)))
        (check (lambda () (locker-value locker))))
    (set! blankq (cons uncheck blankq))
    (work/check-delay eyes-tender check eyes-work-interval)))

check 和 uncheck 函数共享一个 locker 也就是 semaphore。然后 uncheck 函数被加入了 blankq 队列;而 check 函数则被用来做一个小时以后,起动 eyes-tender 之前的检查, 以了解是否用户是持续的工作了一个小时。如果用户不是持续工作一个小时,那么屏幕保护 就会提前起动,这样以来,程序就会调用 blankq 队列里面的每一个函数,然后清空队列:

(lambda ()
  (for-each (lambda (f) (f)) blankq)
  (set! blankq (list)))

这个时候,相应的 uncheck 函数就把相应的 semaphore 的值给设置成了 #f,表示"否"。 这样相应的 check 函数到时候就会返回"否",相应的起动屏幕保护的计划就被撤销了。


运行 shell 命令

下面来说明 (run/port ...) 这个 sexp:

(run/port (xscreensaver-command -watch))

这是 scsh 当中第一个最精彩的部份。它直接起动了一个 shell 命令,在我们这里,就是 指xscreensaver-command 这个命令,加上命令行参数 -watch。然后这个 shell 命令是在 另一个进程空间当中运行的,运行的结果,在当前的这个进程看来,就是一个普通的输入端 口(也即 scheme 语言中所谓的 port。)。

如果是要用 shell 管道起动多个 shell 命令的话,相应的语法就是:

(run (| (echo "hello, world!")
		 (tr hw HW)
		 (cat)))

这就把三个 shell 命令(分别是:一,echo "hello, world!";二、tr hw HW;三、ca t。),给用 UNIX 管道给连接起来了。如果我们直接在 scsh 的交互式环境下运行这个 sexp 的话,我们就得到如下的结果:

zw@trtr:~/qingqing$ scsh
Welcome to scsh 0.6.4 (Olin Shivers)
Type ,? for help.
> (run (| (echo "hello, world!")
          (tr hw HW)
          (cat)))
Hello, World!
0
>
Exit Scsh? (y/n)? y
zw@trtr:~/qingqing$

在后面的例子里面,我们将会看到这个 (run ...) sexp 的更加强大的变形。乍看之下,这 多出来的几个圆括弧,完全失去了传统 UNIX shell 语法的简洁:echo "hello, world!" | tr hw HW | cat 这样不是更加紧凑吗?我们这里需要说明的是,传统的 UNIX shell 的语 法在处理稍微复杂一些的情况的时候,就迅速的失去了它在处理简单的情况的时候所具有的 这样的优雅,而 scsh 的这个语法,完全和 sexp 的概念协调一致,可以毫无困难的扩展到 复杂的多的多的情况。

作为例子,可以考虑 startx < err.log 2<&1;我们立刻注意到,这个尾巴上的 2<&1 是多 么的晦涩。在 scsh 里面,同样的东西写出来:

(run (startx)
     (> err.log)
     (= 2 1))

这样比较起来,可能说服力还是不够。因为其实我们要说明的是 sexp 在处理复杂的情况的 时候,所能体现出来的力量,可是复杂的问题是很难在两、三行的程序当中就提出来的,我 们还是把这个比较放到以后再说吧。8) 在这里不妨再说几句题外话,虽然是题外话,其实 还是相关的。为什么软件设计这个行业内,大多数的真正的高手都不大瞧得起那些程序设计 竞赛呢?也许说"瞧不起"可能是有些"过"了,不过如果说是"带着善意的轻视"的话, 这样应该是不会有错的。这其实就是因为软件行业中真正的困难和真正的挑战都在于要征服 "复杂性",而这个"复杂性"恰恰就是非常难以体现在一个短时间内就要分出胜负的程序 设计竞赛中的。


正则表达式与 awk

下面还是先回到我们的程序,继续看 (awk ...) 这个 sexp:

(awk (read-line port) (line) ()
     ((: bos "BLANK") (blankf))
     ((: bos "UNBLANK") (unblankf)))

这是 scsh 当中第二个最精彩的地方,这是用 sexp 的形式,实现了 awk 语言的功能。awk 我们都知道是 UNIX shell 程序设计中经常使用到的一个程序语言,经常是用来和标准的 shell 配合使用的。现在 scsh 把 awk 也带到了 scheme 的 sexp 的世界里来了。上面的 这个具体的 awk sexp 的例子,是一个循环执行的语句,每次循环,都使用 (read-line port) 从输入端口中用 read-line 函数读入完整的一行,把这一行字符串赋给 line 变量, 然后进入 awk 的正则表达式匹配的阶段。

关于正则表达式,这是 scsh 当中第三个精彩的地方。我们以后将会详细讲到,这里先指出, 上面的两个 sexp,其一,(: bos "BLANK") 表示一个 "BLANK" 开头的字符串;其二,(: bos "UNBLANK") 则表示一个 "UNBLANK" 开头的字符串。这里 bos 的意思是 begin of string。那么,如果这里的某一个正则表达式发生了匹配,就会执行相应的 sexp,以完成 一定的任务。在上面的例子中,我们会分别调用 blankf 或者 unblank 两个函数。


zenity 的对话框

前面说到,我们的闹钟驱动函数 watch-screensaver 在一定的条件得到了满足的情况下, 会调用 eyes-tender 函数,执行一些任务,这个任务就是起动屏幕保护程序,不过,如果 我们很突兀的起动这个屏幕保护程序的话,不免会让用户很吃惊,而且可能也会一下子摸不 清楚到底发生了什么事情。所以我们有必要在起动这个屏幕保护程序之前,先要打出一个对 话框,告诉用户发生了什么事情,我们为什么马上就要看到这个屏幕保护程序起动了。

这个函数起了这么样的一个名字,这是符合我们的目的的。因为,如果屏幕保护程序被起动 了的话,坐在屏幕前面的计算机用户就相当于收到了一个善意的提醒,就可以停下手头正在 计算机上进行的工作,而可以让自己的眼睛得到一会儿功夫的休息了。等到计算机用户觉得 休息够了以后,回来到计算机前面,拍一下键盘,或者晃动一下鼠标,就又可以开始刚才在 计算机上被打断的工作了。

(define (eyes-tender)
  (let ((display-progress
      (lambda ()
        (let lp ((progress 0))
          (case progress
            ((1) (display-line (iconv "# 眼睛该休息啦!")))
            ((50) (display-line (iconv "# 马上就要起动屏幕保护程序了!"))))
          (display-line progress)
          (if (< progress 100)
           (begin (sleep 60)
               (lp (+ 1 progress))))))))
    (run (| (begin (display-progress))
         (zenity --progress
              --width=300
              --auto-close
              ,(string->symbol
                (string-append "--title="
                         (iconv "卿卿爱护你的眼睛"))))))
    (& (xscreensaver-command -activate))))

这个函数其实很简单,就是先调用 zenity 这个 shell 命令,在屏幕上打出一个 GTK+ 2 的对话框,告诉用户一些提醒的话,提醒用户该让眼睛得到休息了,还有马上就要有屏幕保 护程序起动,等等。在这个 GTK+ 2 的对话框完成任务以后,这个函数就会调用前面讲到过 的 xscreensaver-command 这个命令,让屏幕保护程序开始在计算机屏幕上画图。这是经由 下面这个 sexp 完成的:

(& (xscreensaver-command -activate))

开头的 & 把这个命令放到后台运行,不要让自己这个函数老是毫无必要的等待这个 shell 命令的运行结束。


用 zenity 显示 GTK+ 2 的对话框

下面我们来看 zenity 这个命令。这是 GTK+ 2 的一个 shell 命令行工具。这是由太阳微 系统公司为 GNOME 项目开发的。取代了 GNOME 1 中的 gdialog 这个功能相类似的命令。 这个 shell 命令可以让计算机用户在 shell 程序里面就可以调用这个命令,绘出标准的 GTK+ 2 的对话框来。我们在这里要使用的 zenity 的功能,是画出一个进度条。在进度条 增长的过程中,向用户给出一些提示信息。这就是我们要完成的任务。

zenity --progress --width=300 --auto-close --title="some text"

上面的这个 shell 上的 zenity 命令,就可以画出一个进度条对话框。这是由命令行开关 --progress 指定的;--width=300 这个开关则说明了,我们需要的对话框窗口的宽度是 300 像素;--auto-close 这个开关则指明,当我们的进度条走到 100% 满了以后,这个对 话框就会自动关闭;--title="some text" 这个开关则指示我们的对话框窗口的标题为 "some text"。这个 --title 开关看上去似乎是很明显的,应该不会出什么问题。哪知道 就是这个最明显的事情,反而偏偏就出了问题。下面我们就来看这个问题。然后我们再继续 讨论 zenity 的这个进度条对话框的其它问题。


字符串格式转换

GTK+ 2 中间的所有的字符串的传递,都必须是用的 UTF-8 的字符格式编码。而我们一般的 GNU/Linux 上面的中文系统中,最常见的中文字符编码方式是 GB2312 的这个标准的系列。 这样说来的话,我们在把任何字符串传递给 zenity 之前,必须先要把这个字符串的编码格 式从常见的 GB2312 转换为 UTF-8 的格式。为了完成这一个任务,我们要调用 glibc 软件 包中的 iconv 这个命令。我们把对这个命令的调用包装在一个 scsh 函数里面:

(define (iconv str)
  (run/string (iconv -f gb2312 -t utf8) (<< ,str)))

这个函数接收一个 str 参数。然后,在用 run/string 这个 sexp 运行 glibc 中的 iconv 这个 shell 命令的时候,为这个 shell 命令的标准输入作一个重新定向,把这个 shell 命令的标准输入变成为从 str 这个参数中去取出数据。这样以后,这个 shell 命令 iconv 的运行结果,就被 run/string 这个 sexp 收集到,然后作为 scsh 的函数 iconv 的返回 值。下面我们继续看 zenity 的进度条对话框。


zenity 的进度条对话框

zenity --progress 这个命令起来以后,会从标准输入读取一行一行的字符串。如果这一行 字符串的开头是一个数字的话,这个数字就被当作进度条当前要显示出来的进度百分比。如 果这一行的开头是字符"#"的话,后面紧跟着的字符串,就被当作要在对话框上显示的信 息。现在,我们对 zenity 的进度条对话框有了这么样的一个了解以后,接下来需要我们完 成的任务,就是用一个 scsh 函数,按照一个平缓的速度,输出 1 到 100 这些数字,并且 在数字达到一定大小的时候,给出一些更新了的提示信息。这就是下面这个内嵌的函数 display-progress 所做的事情:

(lambda ()
  (let lp ((progress 0))
    (case progress
      ((1) (display-line (iconv "# 眼睛该休息啦!")))
      ((50) (display-line (iconv "# 马上就要起动屏幕保护程序了!"))))
    (display-line progress)
    (if (< progress 100)
        (begin (sleep 60)
               (lp (+ 1 progress))))))))

这个函数使用了 named-let 这个语法结构。这是 scheme 语言中表达循环语句的标准做法。 就是说,把循环用递归调用来表示。我们以后如果有机会详细介绍 scheme 程序语言的话, 就可以对这些方面的内容都做出一个说明。


屏幕快照

在下面的两幅屏幕快照当中,读者朋友们可以看见,正当本文作者在工作的时候,这个用 scsh 和 zenity 编写的简单的"眼睛保护"程序运行起来了呢!


结语

好啦。上面都说完了以后,我们这个建立在"屏幕保护"程序基础之上的"眼睛保护"程序 就算大功告成啦。这是一个非常简单的程序。所以还不大能够体现出来 scheme 语言超越传 统的 UNIX shell 脚本语言在征服"复杂性"方面的优势之处。这篇文章主要的目的是让不 熟悉 scheme 语言的读者朋友们能够对 scheme 程序语言有个大概的印象,能够引起进一步 了解scheme 语言的兴趣;对于熟悉 scheme 程序语言的读者朋友们,则可以了解到 scsh 的一些基本的特点,也可以引起对 scsh 的进一步了解的愿望。

本文的这个例子,的确是还不能完全展示出 scheme 语言和 scsh 的强大的。在以后的文章 里面,读者朋友们将要看到更多的、更有用的 scsh 脚本程序的例子。这些例子将会更加合 适的展示出 scheme 语言和 scsh 的力量。

另一方面,scsh 虽然有上面说的这些好处,但是用 scsh 的环境想要来取代传统 UNIX shell 的直接面对用户的交互式环境,这还是不能够的。传统 UNIX shell 的语法从编写脚 本的角度来说,虽然不能够令人满意,但是从直接的用户交互来说,确实是非常高效的;而 scheme 语言的语法呢,恰恰相反;从编写脚本的角度来看,是非常好;但是如果说,是要 做为一个直接面向用户的、交互式的命令行环境来使用的话,这么多数不清的括号,这是完 全不能胜任一个交互式 shell 环境的要求的。关于这一点,我们在以后的文章中将要介绍 一个解决的办法。这个解决办法,将要最终把 UNIX 系统的威力,经由 scheme 语言的细致 精巧的界面,直接带到用户的手指尖上。

关于 zenity 这个工具,它目前的功能还是十分的有限的。并不能满足稍微复杂一些的图形 用户界面的编程需求。我们在以后的文章里面,将会介绍一个用 ocaml 语言的 GTK+ 2 的 扩展来编写的,功能超级强大的,关于图形用户界面的脚本程序设计的,非常奇妙的工具。 请大家拭目以待!8)


鸣谢

感谢南京大学小百合上的 polymorphism!谢谢你的 eopl2d!手头能有一本印刷出来的专门 讲 scheme 程序语言的书籍,感觉实在是好极啦!

感谢南京大学小百合上的 polymorphism 和 vt 事先阅读了本文的一个草稿!并给予作者重 要的鼓励!谢谢!当然,本文中所有的错误和不尽人意之处还是由作者自己负责的。也欢迎 读者朋友们就这些方面向作者提出意见和建议!


下载文件

iloveqhq-991211.tar.gz : 这是一套用 scsh 语言编写的脚本程序的工具集合,目前 只是一个刚刚起步的版本。其中的 eyes-tender.scm 这是个可以直接运行的 scsh 语言的 脚本程序,这个程序会按照时间提醒用户不要老是坐在计算机前面工作、工作、再工作,而 是要适当的休息一下眼睛。程序的中文名字叫做"卿卿"。"卿卿"很关心你的眼睛健康的 哦!这个程序依赖于 Jamie Zawinski 的 xscreensaver 和太阳微系统公司为 GNOME 项目 开发的 zenity 这两个 shell 命令行工具。这个简单的脚本在 Debian GNU/Linux 的目前 的 testing/unstable 版本上经过初步测试成功。请参照本文前面的说明,进行简单的程序 配置。

需要说明一下的是,这个提供下载的软件包里的 eyes-tender.scm 的实现用到了 scsh 的 模块系统。这个模块系统是 scsh 从 scheme 48 继承来的。而 scheme 48 的模块系统又是 根据 standard ml 的模块系统来开发的。关于这个模块系统,本文由于篇幅的限制,来不 及介绍了。在以后的文章中,将会有这方面内容的进一步介绍。读者朋友们在参考这里的这 个 eyes-tender.scm 文件中的程序代码的时候,可以暂时先忽略这方面的内容,或者自己 先去 scsh 的网站上读一下 scsh 的手册中相关的章节。如果读者朋友们暂时先忽略关于模 块系统这方面的内容,应该并不会对理解 eyes-tender.scm 中的简单的程序代码造成太大 的障碍。

读者朋友们对这个小程序的任何意见或者建议,欢迎发送电子邮件告诉给作者。作者的电子 邮件地址在本文最后列出。这个小程序在这里提供下载的版本是所谓的 Public Domain 的 自由软件。意思是说这是没有任何附加的版权条件的限制的。以后的版本如果会变的比较复 杂的话,可能会考虑到用 GNU GPL 的许可证发布。

参考资料

  • 关于 scheme 程序语言,可以参考 http://www.schemers.org 网站上列出的大量资料。 如果对函数式程序语言有一定的基础的话,可以考虑直接阅读 r5rs 文件。这是 scheme 的 标准定义文件。虽说是个标准文件,但是写的十分的短小精悍,可读性很强。本文作者有一 个专门讨论 scheme 程序语言的中文邮件列表,可以在 http://www.cn99.com 首页上搜 索 scm 就可以看到。欢迎大家参加邮件列表的讨论。
  • man 1 zenity 这是 zenity 的 UNIX 手册页面;还有 zenity 的 GNOME 帮助文档。这两份 文件提供了详尽的 zenity 使用指南,以及简明扼要的使用范例。zenity 使得在 shell 环 境下,进行图形用户界面的程序设计,终于成为一件让人感到还是可以忍受的事情了。当然, zenity 还是只能处理一些简单的、标准的图形用户界面程序的设计任务。如果手头的任务 对图形用户界面的要求比较高的话,像 tcl/tk 一类的工具可能会更加适合一些。
  • xscreensaver http://www.jwz.org/xscreensaver/ 这是 Jamie Zawinski 维护的 X Window 窗口系统的屏幕保护程序。里面包括有许多非常精彩的屏幕保护程序。我们的眼睛 保护程序依赖于这个屏幕保护程序。
  • UNIX 编程的艺术 http://www.catb.org/~esr/writings/taoup/ 这是著名的 Eric S. Raymond 的新书。书中探讨了 UNIX 的软件哲学。感兴趣的读者朋友也可以参照一下著名的 UNIX 厌恶者手册 http://research.microsoft.com/~daniel/unix-haters.html 这本 1994 年出版的旧书。这本厌恶者手册虽说出版的较早,有些过时了,但是和 Raymond 的新书对照起来看,还是饶有趣味的。这两本书的电子版都可以从上面列出的网站上免费下 载阅读。
  • 本文作者以往在 IBM developerWorks 中国网站上发表的,关于函数式程序语言编程的文章:
    http://www.ibm.com/developerworks/cn/linux/sdk/ocaml/part1/index.shtml
    http://www.ibm.com/developerworks/cn/linux/sdk/ocaml/part2/index.shtml
    这两篇文章是用的 ocaml 程序语言介绍了函数式程序设计中的一些基本的概念和技巧。本 文中所提到的 scheme 程序语言也是函数式程序设计语言中的一种。上面提到的这两篇文章 虽然是用的 ocaml 程序语言,而不是 scheme 程序语言,但是由于 ocaml 和 scheme 都是 函数式程序设计语言,上面的这两篇文章对于学习 scheme 程序语言的函数式程序设计的这 个方面还是能有一定的帮助的。目前,国内关于函数式程序语言和程序设计的中文的文章和 书籍还是比较少见的,相信以后这一方面的内容会变得越来越多。函数式的程序设计和程序 语言不仅在日常的编程工作中是非常有用的工具,而且对于程序的形式化以及关于大规模系 统的无错证明等等方面,都有至关重要的应用。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=21936
ArticleTitle=用 scheme 语言进行 UNIX 系统编程
publish-date=12012003