使用Python编写类UNIX系统的命令行工具的教程
引言
您是否能编写命令行工具?也许您可以,但您能编写出真正好用的命令行工具吗?本文讨论使用Python来创建一个强健的命令行工具,并带有内置的帮助菜单、错误处理和选项处理。由于一些奇怪的原因,很多人并不了解Python?的标准库具有制作功能极其强大的*NIX命令行工具所需的全部工具。
可以这样说,Python是制作*NIX命令行工具的最佳语言,因为它依照“batteries-included”的哲学方式工作,并且强调提供可读性高的代码。但仅作为提醒,当您发现使用Python创建命令行工具是一件多么简单的事情时,这些想法很危险,您的生活可能被搅得一团糟。据我所知,至今还没有发表过详细说明使用Python创建命令行工具的文章,因此我希望您喜欢这篇文章。
设置
Python标准库中的optparse模块可完成创建命令行工具的大部分琐碎工作。optparse包含在Python2.3中,因此该模块将包括在许多*NIX操作系统中。如果由于某种原因,您使用的操作系统不包含所需要的模块,那么值得庆幸的是,Python的最新版本已经过测试并编译到几乎任何*NIX操作系统中。Python支持的系统包括IBM?AIX?、HP-UX、Solaris、FreeBSD、RedHatLinux?、Ubuntu、OSX、IRIX,甚至包括几种Nokia手机。
创建HelloWorld命令行工具
编写优秀的命令行工具的第一步是定义要解决的问题。这对您工具的成功至关重要。这对于以尽可能简单的方法解决问题也同样重要。这里明确地采用了KISS(KeepItSimpleStupid,保持简单)准则。只有在实现并测试了计划内功能之后才添加选项和增加其他功能。
我们首先从创建HelloWorld命令行工具开始。按照上面的建议,我们使用尽可能简单的术语来定义问题。
问题定义:我希望创建一个命令行工具,默认打印HelloWorld,并提供用于打印不通人的姓名的选项。
基于上述说明,可以提供一个包含少量代码的解决方案。
HelloWorld命令行接口(CLI)
#!/usr/bin/envpython importoptparse defmain(): p=optparse.OptionParser() p.add_option('--person','-p',default="world") options,arguments=p.parse_args() print'Hello%s'%options.person if__name__=='__main__': main()
如果运行此代码,预期的输出如下:
Helloworld
但是,我们通过少量代码所能做到的远不止于此。我们可以获得自动生成的帮助菜单:
pythonhello_cli.py--help Usage:hello_cli.py[options] Options: -h,--helpshowthishelpmessageandexit -pPERSON,--person=PERSON
从帮助菜单中可以了解到,我们可以使用两种方法来更改HelloWorld的输出:
pythonhello_cli.py-pguido Helloguido
我们还实现了自动生成的错误处理:
pythonhello_cli.py--namematz Usage:hello_cli.py[options] hello_cli.py:error:nosuchoption:--name
如果您还没有使用过Python的optparse模块,那么您刚才可能会大吃一惊,并思忖使用Python可以编写的所有这些不可思议的工具。如果您刚开始接触Python,那么您可能会惊讶于Python让一切变得如此简单。“XKCD”网站发表了关于“Python是如此简单”主题的非常有趣的漫画,已包括在参考资料中。
创建有用的命令行工具
既然我们已经打好了基础,我们就可以继续创建解决特定问题的工具。对于本例,我们将使用Python的名为Scapy的网络库和交互式工具。Scapy可以在大多数*NIX系统上正常工作,可以在第2层和第3层上发送数据包,并允许您创建只有几行Python代码的非常复杂的工具。如果您希望按部就班从头开始,请确保您正确地安装了必要的软件。
我们先定义要解决的新问题。
问题:我希望创建一个使用IP地址或子网作为参数的命令行工具,并向标准输出返回MAC地址或MAC地址列表以及它们各自的IP地址。
既然我们已经清楚地定义了问题,让我尝试将问题分解为尽可能简单的部分,然后逐一解决这些部分。对于这一问题,我看到了两个独立的部分。第一部分是编写接收IP地址或子网范围的函数,并返回MAC地址或MAC地址列表。我们可以在解决此问题之后再考虑将其集成到命令行工具中。
解决方案第1部分:创建通过IP地址确定MAC地址的Python函数
arping fromscapyimportsrp,Ether,ARP,conf conf.verb=0 ans,unans=srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="10.0.1.1"), timeout=2) forsnd,rcvinans: printrcv.sprintf(r"%Ether.src%%ARP.psrc%")
该命令的输出是:
sudopythonarping.py 00:00:00:00:00:0110.0.1.1
请注意,使用scapy执行操作要求提升的权限,因此我们必须使用sudo。考虑到本文的目的,我还将实际输出更改为包括伪MAC地址。我们已经证实了我们可以通过IP地址找到MAC地址。我们需要整理此代码以接受IP地址或子网并返回MAC地址和IP地址对。
arping函数
#!/usr/bin/envpython fromscapyimportsrp,Ether,ARP,conf defarping(iprange="10.0.1.0/24"): conf.verb=0 ans,unans=srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst=iprange), timeout=2) collection=[] forsnd,rcvinans: result=rcv.sprintf(r"%ARP.psrc%%Ether.src%").split() collection.append(result) returncollection #Printresults values=arping() forip,macinvalues: printip,mac
正如您看到的,我们编写了一个函数,该函数接受IP地址或网络并返回嵌套的IP/MAC地址列表。我们现已为第二部分做好准备,为我们的工具创建一个命令行接口。
解决方案第2部分:从我们的arping函数创建命令行工具
在本例中,我们综合本文前面部分的想法,创建一个能解决我们初始问题的完整命令行工具。
arpingCLI
#!/usr/bin/envpython importoptparse fromscapyimportsrp,Ether,ARP,conf defarping(iprange="10.0.1.0/24"): """ArpingfunctiontakesIPAddressorNetwork,returnsnestedmac/iplist""" conf.verb=0 ans,unans=srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst=iprange), timeout=2) collection=[] forsnd,rcvinans: result=rcv.sprintf(r"%ARP.psrc%%Ether.src%").split() collection.append(result) returncollection defmain(): """Runsprogramandhandlescommandlineoptions""" p=optparse.OptionParser(description='FindsMACAddressofIPaddress(es)', prog='pyarping', version='pyarping0.1', usage='%prog[10.0.1.1or10.0.1.0/24]') options,arguments=p.parse_args() iflen(arguments)==1: values=arping(iprange=arguments) forip,macinvalues: printip,mac else: p.print_help() if__name__=='__main__': main()
对以上脚本进行几点说明将有助于我们了解optparse的工作方式。
首先,必须创建optparse.OptionParser()的一个实例,并且接受如下所示的可选参数:
description,prog,version,andusage
这些参数的含义基本上可以不言自明,但我希望确认的一点是,您应该了解optparse虽然功能强大,但并不是无所不能。它具有明确定义的接口,可用于快速创建命令行工具。
其次,在如下行中:
options,arguments=p.parse_args()
该行的作用是将选项和参数划分为不同的位。在上述代码中,我们预期恰有一个参数,因此我指定必须只有一个参数值,并将该值传递给arping函数。
iflen(arguments)==1: values=arping(iprange=arguments)
为了进一步说明,让我们运行下面的命令以了解其工作方式:
sudopythonarping.py10.0.1.1 10.0.1.100:00:00:00:00:01
在上述示例中,参数为10.0.1.1,由于正如我在条件语句中指定的那样只有一个参数,因此该参数被传递给arping函数。如果存在选项,它们将在options,arguments=p.parse_args()方法中传递给options。让我们看一下,当我们分解命令行工具的预期用例并赋予该用例两个参数时将会发生什么情况:
sudopythonarping.py10.0.1.110.0.1.3 Usage:pyarping[10.0.1.1or10.0.1.0/24] FindsMACAddressorIPaddress(es) Options: --versionshowprogram'sversionnumberandexit -h,--helpshowthishelpmessageandexit
根据我为参数构建的条件语句的结构,如果参数的数目不为1,它将自动打开帮助菜单:
iflen(arguments)==1: values=arping(iprange=arguments) forip,macinvalues: printip,mac else: p.print_help()
这是一种用于控制工具的工作方式的重要方法,因为您可以使用参数的个数或特定选项的名称作为控制命令行工具的流程的机制。因为我们在最初的HelloWorld示例中涉及了选项的创建,接下来通过略微更改主函数向我们的命令行工具添加几个选项:
arpingCLImain函数
defmain(): """Runsprogramandhandlescommandlineoptions""" p=optparse.OptionParser(description='FindsMACAddressofIPaddress(es)', prog='pyarping', version='pyarping0.1', usage='%prog[10.0.1.1or10.0.1.0/24]') p.add_option('-m','--mac',action='store_true',help='returnsonlymacaddress') p.add_option('-v','--verbose',action='store_true',help='returnsverboseoutput') options,arguments=p.parse_args() iflen(arguments)==1: values=arping(iprange=arguments) ifoptions.mac: forip,macinvalues: printmac elifoptions.verbose: forip,macinvalues: print"IP:%sMAC:%s"%(ip,mac) else: forip,macinvalues: printip,mac else: p.print_help()
所做的主要更改是创建了基于是否指定了某个选项的条件语句。请注意,与HelloWorld命令行工具不同,我们仅使用选项作为我们工具的true/false信号。对于–MAC选项的情况,如果指定了该选项,我们的条件语句elif将只打印MAC地址。
下面是新选项的输出:
arping输出
sudopythonarping2.py Password: Usage:pyarping[10.0.1.1or10.0.1.0/24] FindsMACAddressofIPaddress(es) Options: --versionshowprogram'sversionnumberandexit -h,--helpshowthishelpmessageandexit -m,--macreturnsonlymacaddress -v,--verbosereturnsverboseoutput [ngift@M-6][H:11184][J:0]>sudopythonarping2.py10.0.1.1 10.0.1.100:00:00:00:00:01 [ngift@M-6][H:11185][J:0]>sudopythonarping2.py-m10.0.1.1 00:00:00:00:00:01 [ngift@M-6][H:11186][J:0]>sudopythonarping2.py-v10.0.1.1 IP:10.0.1.1MAC:00:00:00:00:00:01
深入学习创建命令行工具
下面是几个用于深入学习的新想法。在我正与别人合著的有关Python*NIX系统管理的书中对这些想法进行了深入的探讨,该书将在2008年中期出版。
在命令行工具中使用subprocess模块
subprocess模块包括在Python2.4或更高版本中,是用于处理系统调用和流程的统一接口。您可以轻松替换上面的arping函数,以使用适用于您的特定*NIX操作系统的arping工具。以下是体现上述想法的粗略示例:
子流程arping
importsubprocess importre defarping(ipaddress="10.0.1.1"): """ArpingfunctiontakesIPAddressorNetwork,returnsnestedmac/iplist""" #AssuminguseofarpingonRedHatLinux p=subprocess.Popen("/usr/sbin/arping-c2%s"%ipaddress,shell=True, stdout=subprocess.PIPE) out=p.stdout.read() result=out.split() pattern=re.compile(":") foriteminresult: ifre.search(pattern,item): printitem arping()
以下是该函数单独运行时的输出:[root@localhost]~#pythonpyarp.py[00:16:CB:C3:B4:10]
请注意使用subprocess来获取arping命令的输出,以及使用已编译的正则表达式匹配MAC地址。注意,如果您使用的是Python2.3,则可以使用popen模块替换subprocess,后者在Python2.4或更高版本中提供。
在命令行工具中使用对象关系映射器,如配合SQLite使用的SQLAlchemy或Storm
命令行工具的另一个可能选项是使用ORM(对象关系映射器)来存储由命令行工具生成的数据记录。有相当多的ORM可用于Python,但SQLAlchemy和Storm恰好是最常用的两个。我通过掷硬币的方式决定使用Storm作为示例:
StormORMarping
#!/usr/bin/envpython importoptparse fromstorm.localsimport* fromscapyimportsrp,Ether,ARP,conf classNetworkRecord(object): __storm_table__="networkrecord" id=Int(primary=True) ip=RawStr() mac=RawStr() hostname=RawStr() defarping(iprange="10.0.1.0/24"): """ArpingfunctiontakesIPAddressorNetwork, returnsnestedmac/iplist""" conf.verb=0 ans,unans=srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst=iprange), timeout=2) collection=[] forsnd,rcvinans: result=rcv.sprintf(r"%ARP.psrc%%Ether.src%").split() collection.append(result) returncollection defmain(): """Runsprogramandhandlescommandlineoptions""" p=optparse.OptionParser() p=optparse.OptionParser(description='FindsMACAddrofIPaddress(es)', prog='pyarping', version='pyarping0.1', usage='%prog[10.0.1.1or10.0.1.0/24]') options,arguments=p.parse_args() iflen(arguments)==1: database=create_database("sqlite:") store=Store(database) store.execute("CREATETABLEnetworkrecord" "(idINTEGERPRIMARYKEY,ipVARCHAR,\ macVARCHAR,hostnameVARCHAR)") values=arping(iprange=arguments) machine=NetworkRecord() store.add(machine) #CreatesRecords forip,macinvalues: machine.mac=mac machine.ip=ip #Flushestodatabase store.flush() #PrintsRecord print"RecordNumber:%r"%machine.id print"MACAddress:%r"%machine.mac print"IPAddress:%r"%machine.ip else: p.print_help() if__name__=='__main__': main()
本例中需要关注的主要内容是创建名为NetworkRecord的类,该类映射到“内存中”的SQLite数据库。在main函数中,我将arping函数的输出更改为映射到我们的记录对象,将它们更新到数据库,然后再将其取回以打印结果。这明显不是一个可用于生产的工具,但可作为在我们的工具中使用ORM的相关步骤的说明性示例。
在CLI中集成config文件
PythonINIconfig语法
[AIX] MAC:00:00:00:00:02 IP:10.0.1.2 Hostname:aix.example.com [HPUX] MAC:00:00:00:00:03 IP:10.0.1.3 Hostname:hpux.example.com [SOLARIS] MAC:00:00:00:00:04 IP:10.0.1.4 Hostname:solaris.example.com [REDHAT] MAC:00:00:00:00:05 IP:10.0.1.5 Hostname:redhat.example.com [UBUNTU] MAC:00:00:00:00:06 IP:10.0.1.6 Hostname:ubuntu.example.com [OSX] MAC:00:00:00:00:07 IP:10.0.1.7 Hostname:osx.example.com
接下来,我们需要使用ConfigParser模块来解析上述内容:
ConfigParser函数
#!/usr/bin/envpython importConfigParser defreadConfig(file="config.ini"): Config=ConfigParser.ConfigParser() Config.read(file) sections=Config.sections() formachineinsections: #uncommentlinebelowtoseehowthisconfigfileisparsed #printConfig.items(machine) macAddr=Config.items(machine)[0][1] printmachine,macAddr readConfig()
该函数的输出如下:
OSX00:00:00:00:07 SOLARIS00:00:00:00:04 AIX00:00:00:00:02 REDHAT00:00:00:00:05 UBUNTU00:00:00:00:06 HPUX00:00:00:00:03
我将剩下的问题作为练习留给读者来解决。我接下来要做的是将该config文件集成到我的脚本中,这样我就可以将我的config文件中记录的机器库存与出现在ARP缓存中的MAC地址的实际库存进行比较。IP地址或主机名只在跟踪到计算机时才能发挥其作用,但是我们实现的工具对于跟踪网络上存在的计算机的硬件地址并确定它以前是否出现在网络上可能非常有用。
结束语
我们首先通过编写几行代码创建了一个非常简单但功能强大的HelloWorld命令行工具。然后使用Python网络库创建了一个复杂的网络工具。最后,我们继续讨论一些更高级的研究领域以飨读者。在高级研究部分,我们讨论了subprocess模块、对象关系映射器的集成,最后讨论了配置文件。
虽然并不为众人所知,但任何具有IT背景的读者都可以使用Python轻松地创建命令行工具。我希望本文能够激励您亲自动手创建全新的命令行工具。