第 9 章:使用 Jython 脚本

在本章中,我们将探讨使用 Jython 进行脚本编写。就我们的目的而言,我将把“脚本编写”定义为编写小型程序来帮助完成日常任务。这些任务包括删除和创建目录、管理文件和程序,或者任何其他你可能能够用小型程序表达的重复性任务。然而,在实践中,脚本可能会变得非常庞大,以至于脚本和完整程序之间的界限变得模糊。

我们将从概述一些 Jython 附带的、对这些任务最有用的一些模块开始。这些模块包括 os、shutil、getopt、optparse、subprocess。我们只是让你对这些模块有一个简单的了解。有关详细信息,你应该查看参考文档,例如 http://jyton.org/currentdocs。然后我们将介绍一个中等规模的任务,以展示如何将这些模块中的几个一起使用。

解析命令行选项

许多脚本都是简单的单次使用脚本,你只需编写一次,使用一次,然后就忘记了。其他的脚本随着时间的推移会成为你每周甚至每天使用的部分。当你发现自己反复使用一个脚本时,你会发现传递命令行选项很有帮助。在 Jython 中,这主要有三种方法。第一种方法是手动解析参数,第二种方法是使用 getopt 模块,第三种方法是使用更新、更灵活的 optparse 模块。

假设我们有一个名为 foo.py 的脚本,并且你希望在调用它时能够传递一些参数,脚本名称和传递的参数可以通过导入 sys 模块并检查 sys.argv 来查看,如下所示

# 脚本 foo.py import sys print sys.argv

如果你使用 a、b 和 c 作为参数运行上述脚本

$ jython foo.py a b c
$ ['foo.py', 'a', 'b', 'c']

脚本名称最终出现在 sys.argv[0] 中,其余参数出现在 sys.argv[1:] 中。通常你会在 Jython 程序中看到以下代码

# 脚本 foo2.py import sys

args = sys.argv[1:] print args

这将导致

$ jython foo2.py a b c
$ ['a', 'b', 'c']

如果你要做的不仅仅是直接将参数传递给你的脚本,那么手动解析这些参数可能会变得相当繁琐。Jython 库包含两个模块,你可以使用它们来避免繁琐的手动解析。这些模块是 getopt 和 optparse。optparse 模块是更新、更灵活的选择,因此我将重点介绍它。getopt 模块仍然有用,因为它对于更简单的预期参数需要更少的代码。以下是一个基本的 optparse 脚本

# script foo3.py
from optparse import optionparser
parser = optionparser()
parser.add_option("-f", "--foo" help="set foo option")
parser.add_option("-b", "--bar" help="set bar option")
(options, args) = parser.parse_args()
print "options: %s" % options
print "args: %s" % args

运行上述脚本

$ jython foo3.py -b a --foo b c d
$ options: {'foo': 'b', 'bar': 'a'}
$ args: ['c', 'd']

我将在本章后面使用更具体的示例来介绍 optparse 模块。

脚本化文件系统

我们将从可能对文件系统执行的最简单操作开始,即列出目录的文件内容。

>>> import os
>>> os.listdir('.')
['ast', 'doc', 'grammar', 'lib', 'license.txt', 'news', 'notice.txt', 'src']

首先,我们导入了 os 模块,然后在当前目录(用“.”表示)上执行了 listdir。当然,您的输出将反映您本地目录的内容。os 模块包含许多您期望用于处理操作系统的函数。os.path 模块包含有助于处理文件系统路径的函数。

编译 Java 源代码

虽然编译 Java 源代码并非严格意义上的典型脚本任务,但它是我想在我的下一个部分开始的更大型示例中展示的任务。我将要介绍的 API 是在 jdk 6 中引入的,并且 JVM 供应商可以选择实现它。我知道它在 sun 的 jdk 6 和 mac os x 附带的 jdk 6 上运行。有关 javacompiler API 的更多详细信息,一个好的起点是这里:http://java.sun.com/javase/6/docs/api/javax/tools/javacompiler.html。以下是使用 jython 的此 API 的简单示例

compiler = toolprovider.getsystemjavacompiler()
diagnostics = diagnosticcollector()
manager = compiler.getstandardfilemanager(diagnostics, none, none)
units = manager.getjavafileobjectsfromstrings(names)
comp_task = compiler.gettask(none, manager, diagnostics, none, none, units)
success = comp_task.call()
manager.close()

示例脚本:builder.py

因此,我已经讨论了一些在为 jython 编写脚本时往往会派上用场的模块。现在,我将编写一个简单的脚本,以展示可以完成的操作。我选择编写一个脚本,该脚本将帮助处理将 java 文件编译为 .class 文件到目录中,并将目录中的 .class 文件作为单独的任务清理。我将希望能够创建目录结构,删除目录结构以进行干净的构建,当然还有编译我的 java 源文件。

import os
import sys
import glob

from javax.tools import (forwardingjavafilemanager, toolprovider,
        diagnosticcollector,)

tasks = {}

def task(func):
    tasks[func.func_name] = func

@task
def clean():
    files = glob.glob("*.class")
    for file in files:
        os.unlink(file)

@task
def compile():
    files = glob.glob("*.java")
    _log("compiling %s" % files)
    if not _compile(files):
        quit()
    _log("compiled")

def _log(message):
    if options.verbose:
        print message

def _compile(names):
    compiler = toolprovider.getsystemjavacompiler()
    diagnostics = diagnosticcollector()
    manager = compiler.getstandardfilemanager(diagnostics, none, none)
    units = manager.getjavafileobjectsfromstrings(names)
    comp_task = compiler.gettask(none, manager, diagnostics, none, none, units)
    success = comp_task.call()
    manager.close()
    return success

if __name__ == '__main__':
    from optparse import optionparser
    parser = optionparser()
    parser.add_option("-q", "--quiet",
            action="store_false", dest="verbose", default=true,
            help="don't print out task messages.")
    parser.add_option("-p", "--projecthelp",
            action="store_true", dest="projecthelp",
            help="print out list of tasks.")
    (options, args) = parser.parse_args()

    if options.projecthelp:
        for task in tasks:
            print task
        sys.exit(0)

    if len(args) < 1:
        print "usage: jython builder.py [options] task"
        sys.exit(1)
    try:
        current = tasks[args[0]]
    except keyerror:
        print "task %s not defined." % args[0]
        sys.exit(1)
    current()

该脚本定义了一个“task”装饰器,它收集函数的名称并将它们放入字典中。我们有一个 optionparser 类,它定义了两个选项 -projecthelp 和 -quiet。默认情况下,脚本将其操作记录到标准输出。-quiet 关闭此记录。-projecthelp 列出可用的任务。我们定义了两个任务,“compile”和“clean”。“compile”任务将所有目录中的 .java 文件进行 glob 并编译它们。“clean”任务将所有目录中的 .class 文件进行 glob 并删除它们。请注意!.class 文件将被删除,不会提示您!

所以让我们试一试。如果您在与 builer.py 相同的目录中创建了一个 Java 类,比如经典的“Hello World”程序

HelloWorld.java

public class HelloWorld {
   public static void main(String[] args) {
       System.out.println("Hello, World");
   }
}

然后,您可以对 builder.py 发出以下命令,并获得以下结果

[frank@pacman chapter8]$ jython builder.py --help
Usage: builder.py [options]

Options:
  -h, --help         show this help message and exit
  -q, --quiet        Don't print out task messages.
  -p, --projecthelp  Print out list of tasks.
[frank@pacman chapter8]$ jython builder.py --projecthelp
compile
clean
[frank@pacman chapter8]$ jython builder.py compile
compiling ['HelloWorld.java']
compiled
[frank@pacman chapter8]$ ls
DEBUG.classicHelloWorld.java
HelloWorld.classicHelloWorldbuilder.py
[frank@pacman chapter8]$ jython builder.py clean
[frank@pacman chapter8]$ ls
HelloWorld.javabuilder.py
[frank@pacman chapter8]$ jython builder.py --quiet compile
[frank@pacman chapter8]$ ls
DEBUG.classicHelloWorldHelloWorld.java
HelloWorld.classicHelloWorldHelloWorldbuilder.py
[frank@pacman chapter8]$