引言
您是否能編寫命令行工具?也許您可以,但您能編寫出真正好用的命令行工具嗎?本文討論使用 Python 來創建一個強健的命令行工具,并帶有內置的幫助菜單、錯誤處理和選項處理。由于一些奇怪的原因,很多人并不了解 Python? 的標準庫具有制作功能極其強大的 *NIX 命令行工具所需的全部工具。
可以這樣說,Python 是制作 *NIX 命令行工具的最佳語言,因為它依照“batteries-included”的哲學方式工作,并且強調提供可讀性高的代碼。但僅作為提醒,當您發現使用 Python 創建命令行工具是一件多么簡單的事情時,這些想法很危險,您的生活可能被攪得一團糟。據我所知,至今還沒有發表過詳細說明使用 Python 創建命令行工具的文章,因此我希望您喜歡這篇文章。
設置
Python 標準庫中的 optparse 模塊可完成創建命令行工具的大部分瑣碎工作。optparse 包含在 Python 2.3 中,因此該模塊將包括在許多 *NIX 操作系統中。如果由于某種原因,您使用的操作系統不包含所需要的模塊,那么值得慶幸的是,Python 的最新版本已經過測試并編譯到幾乎任何 *NIX 操作系統中。Python 支持的系統包括 IBM? AIX?、HP-UX、Solaris、Free BSD、Red Hat Linux?、Ubuntu、OS X、IRIX,甚至包括幾種 Nokia 手機。
創建 Hello World 命令行工具
編寫優秀的命令行工具的第一步是定義要解決的問題。這對您工具的成功至關重要。這對于以盡可能簡單的方法解決問題也同樣重要。這里明確地采用了 KISS(Keep It Simple Stupid,保持簡單)準則。只有在實現并測試了計劃內功能之后才添加選項和增加其他功能。
我們首先從創建 Hello World 命令行工具開始。按照上面的建議,我們使用盡可能簡單的術語來定義問題。
問題定義:我希望創建一個命令行工具,默認打印 Hello World,并提供用于打印不通人的姓名的選項。
基于上述說明,可以提供一個包含少量代碼的解決方案。
Hello World 命令行接口 (CLI)
1
2
3
4
5
6
7
8
9
10
11
|
#!/usr/bin/env python import optparse def main(): p = optparse.OptionParser() p.add_option( '--person' , '-p' , default = "world" ) options, arguments = p.parse_args() print 'Hello %s' % options.person if __name__ = = '__main__' : main() |
如果運行此代碼,預期的輸出如下:
1
|
Hello world |
但是,我們通過少量代碼所能做到的遠不止于此。我們可以獲得自動生成的幫助菜單:
1
2
3
4
5
6
|
python hello_cli.py - - help Usage: hello_cli.py [options] Options: - h, - - help show this help message and exit - p PERSON, - - person = PERSON |
從幫助菜單中可以了解到,我們可以使用兩種方法來更改 Hello World 的輸出:
1
2
|
python hello_cli.py -p guido Hello guido |
我們還實現了自動生成的錯誤處理:
1
2
3
4
|
python hello_cli.py --name matz Usage: hello_cli.py [options] hello_cli.py: error: no such option: --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 函數
1
2
3
4
5
6
7
8
9
10
|
arping from scapy import srp,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 ) for snd, rcv in ans: print rcv.sprintf(r "%Ether.src% %ARP.psrc%" ) |
該命令的輸出是:
1
2
|
sudo python arping.py 00:00:00:00:00:01 10.0.1.1 |
請注意,使用 scapy 執行操作要求提升的權限,因此我們必須使用 sudo。考慮到本文的目的,我還將實際輸出更改為包括偽 MAC 地址。我們已經證實了我們可以通過 IP 地址找到 MAC 地址。我們需要整理此代碼以接受 IP 地址或子網并返回 MAC 地址和 IP 地址對。
arping 函數
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
#!/usr/bin/env python from scapy import srp,Ether,ARP,conf def arping(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 = [] for snd, rcv in ans: result = rcv.sprintf(r "%ARP.psrc% %Ether.src%" ).split() collection.append(result) return collection #Print results values = arping() for ip,mac in values: print ip,mac |
正如您看到的,我們編寫了一個函數,該函數接受 IP 地址或網絡并返回嵌套的 IP/MAC 地址列表。我們現已為第二部分做好準備,為我們的工具創建一個命令行接口。
解決方案第 2 部分:從我們的 arping 函數創建命令行工具
在本例中,我們綜合本文前面部分的想法,創建一個能解決我們初始問題的完整命令行工具。
arping CLI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
#!/usr/bin/env python import optparse from scapy import srp,Ether,ARP,conf def arping(iprange = "10.0.1.0/24" ): """Arping function takes IP Address or Network, returns nested mac/ip list""" conf.verb = 0 ans,unans = srp(Ether(dst = "ff:ff:ff:ff:ff:ff" ) / ARP(pdst = iprange), timeout = 2 ) collection = [] for snd, rcv in ans: result = rcv.sprintf(r "%ARP.psrc% %Ether.src%" ).split() collection.append(result) return collection def main(): """Runs program and handles command line options""" p = optparse.OptionParser(description = ' Finds MAC Address of IP address(es)' , prog = 'pyarping' , version = 'pyarping 0.1' , usage = '%prog [10.0.1.1 or 10.0.1.0/24]' ) options, arguments = p.parse_args() if len (arguments) = = 1 : values = arping(iprange = arguments) for ip, mac in values: print ip, mac else : p.print_help() if __name__ = = '__main__' : main() |
對以上腳本進行幾點說明將有助于我們了解 optparse 的工作方式。
首先,必須創建 optparse.OptionParser() 的一個實例,并且接受如下所示的可選參數:
這些參數的含義基本上可以不言自明,但我希望確認的一點是,您應該了解 optparse 雖然功能強大,但并不是無所不能。它具有明確定義的接口,可用于快速創建命令行工具。
其次,在如下行中:
該行的作用是將選項和參數劃分為不同的位。在上述代碼中,我們預期恰有一個參數,因此我指定必須只有一個參數值,并將該值傳遞給 arping 函數。
1
2
|
if len (arguments) = = 1 : values = arping(iprange = arguments) |
為了進一步說明,讓我們運行下面的命令以了解其工作方式:
1
2
3
|
sudo python arping.py 10.0.1.1 10.0.1.1 00:00:00:00:00:01 |
在上述示例中,參數為 10.0.1.1,由于正如我在條件語句中指定的那樣只有一個參數,因此該參數被傳遞給 arping 函數。如果存在選項,它們將在 options, arguments = p.parse_args() 方法中傳遞給 options。讓我們看一下,當我們分解命令行工具的預期用例并賦予該用例兩個參數時將會發生什么情況:
1
2
3
4
5
6
7
8
|
sudo python arping.py 10.0.1.1 10.0.1.3 Usage: pyarping [10.0.1.1 or 10.0.1.0 /24 ] Finds MAC Address or IP address(es) Options: --version show program's version number and exit -h, --help show this help message and exit |
根據我為參數構建的條件語句的結構,如果參數的數目不為 1,它將自動打開幫助菜單:
1
2
3
4
5
6
|
if len (arguments) = = 1 : values = arping(iprange = arguments) for ip, mac in values: print ip, mac else : p.print_help() |
這是一種用于控制工具的工作方式的重要方法,因為您可以使用參數的個數或特定選項的名稱作為控制命令行工具的流程的機制。因為我們在最初的 Hello World 示例中涉及了選項的創建,接下來通過略微更改主函數向我們的命令行工具添加幾個選項:
arping CLI main 函數
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
def main(): """Runs program and handles command line options""" p = optparse.OptionParser(description = 'Finds MAC Address of IP address(es)' , prog = 'pyarping' , version = 'pyarping 0.1' , usage = '%prog [10.0.1.1 or 10.0.1.0/24]' ) p.add_option( '-m' , '--mac' , action = 'store_true' , help = 'returns only mac address' ) p.add_option( '-v' , '--verbose' , action = 'store_true' , help = 'returns verbose output' ) options, arguments = p.parse_args() if len (arguments) = = 1 : values = arping(iprange = arguments) if options.mac: for ip, mac in values: print mac elif options.verbose: for ip, mac in values: print "IP: %s MAC: %s " % (ip, mac) else : for ip, mac in values: print ip, mac else : p.print_help() |
所做的主要更改是創建了基于是否指定了某個選項的條件語句。請注意,與 Hello World 命令行工具不同,我們僅使用選項作為我們工具的 true/false 信號。對于 –MAC 選項的情況,如果指定了該選項,我們的條件語句 elif 將只打印 MAC 地址。
下面是新選項的輸出:
arping 輸出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
sudo python arping2.py Password: Usage: pyarping [ 10.0 . 1.1 or 10.0 . 1.0 / 24 ] Finds MAC Address of IP address(es) Options: - - version show program's version number and exit - h, - - help show this help message and exit - m, - - mac returns only mac address - v, - - verbose returns verbose output [ngift@M - 6 ][H: 11184 ][J: 0 ]> sudo python arping2.py 10.0 . 1.1 10.0 . 1.1 00 : 00 : 00 : 00 : 00 : 01 [ngift@M - 6 ][H: 11185 ][J: 0 ]> sudo python arping2.py - m 10.0 . 1.1 00 : 00 : 00 : 00 : 00 : 01 [ngift@M - 6 ][H: 11186 ][J: 0 ]> sudo python arping2.py - v 10.0 . 1.1 IP: 10.0 . 1.1 MAC: 00 : 00 : 00 : 00 : 00 : 01 |
深入學習創建命令行工具
下面是幾個用于深入學習的新想法。在我正與別人合著的有關 Python *NIX 系統管理的書中對這些想法進行了深入的探討,該書將在 2008 年中期出版。
在命令行工具中使用 subprocess 模塊
subprocess 模塊包括在 Python 2.4 或更高版本中,是用于處理系統調用和流程的統一接口。您可以輕松替換上面的 arping 函數,以使用適用于您的特定 *NIX 操作系統的 arping 工具。以下是體現上述想法的粗略示例:
子流程 arping
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import subprocess import re def arping(ipaddress = "10.0.1.1" ): """Arping function takes IP Address or Network, returns nested mac/ip list""" #Assuming use of arping on Red Hat Linux p = subprocess.Popen( "/usr/sbin/arping -c 2 %s" % ipaddress, shell = True , stdout = subprocess.PIPE) out = p.stdout.read() result = out.split() pattern = re. compile ( ":" ) for item in result: if re.search(pattern, item): print item arping() |
以下是該函數單獨運行時的輸出: [root@localhost]~# python pyarp.py [00:16:CB:C3:B4:10]
請注意使用 subprocess 來獲取 arping 命令的輸出,以及使用已編譯的正則表達式匹配 MAC 地址。注意,如果您使用的是 Python 2.3,則可以使用 popen 模塊替換 subprocess,后者在 Python 2.4 或更高版本中提供。
在命令行工具中使用對象關系映射器,如配合 SQLite 使用的 SQLAlchemy 或 Storm
命令行工具的另一個可能選項是使用 ORM(對象關系映射器)來存儲由命令行工具生成的數據記錄。有相當多的 ORM 可用于 Python,但 SQLAlchemy 和 Storm 恰好是最常用的兩個。我通過擲硬幣的方式決定使用 Storm 作為示例:
Storm ORM arping
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
#!/usr/bin/env python import optparse from storm. locals import * from scapy import srp,Ether,ARP,conf class NetworkRecord( object ): __storm_table__ = "networkrecord" id = Int (primary = True ) ip = RawStr() mac = RawStr() hostname = RawStr() def arping(iprange = "10.0.1.0/24" ): """Arping function takes IP Address or Network, returns nested mac/ip list""" conf.verb = 0 ans,unans = srp(Ether(dst = "ff:ff:ff:ff:ff:ff" ) / ARP(pdst = iprange), timeout = 2 ) collection = [] for snd, rcv in ans: result = rcv.sprintf(r "%ARP.psrc% %Ether.src%" ).split() collection.append(result) return collection def main(): """Runs program and handles command line options""" p = optparse.OptionParser() p = optparse.OptionParser(description = 'Finds MACAddr of IP address(es)' , prog = 'pyarping' , version = 'pyarping 0.1' , usage = '%prog [10.0.1.1 or 10.0.1.0/24]' ) options, arguments = p.parse_args() if len (arguments) = = 1 : database = create_database( "sqlite:" ) store = Store(database) store.execute( "CREATE TABLE networkrecord " "( id INTEGER PRIMARY KEY, ip VARCHAR,\ mac VARCHAR, hostname VARCHAR)") values = arping(iprange = arguments) machine = NetworkRecord() store.add(machine) #Creates Records for ip, mac in values: machine.mac = mac machine.ip = ip #Flushes to database store.flush() #Prints Record print "Record Number: %r" % machine. id print "MAC Address: %r" % machine.mac print "IP Address: %r" % machine.ip else : p.print_help() if __name__ = = '__main__' : main() |
本例中需要關注的主要內容是創建名為 NetworkRecord 的類,該類映射到“內存中”的 SQLite 數據庫。在 main 函數中,我將 arping 函數的輸出更改為映射到我們的記錄對象,將它們更新到數據庫,然后再將其取回以打印結果。這明顯不是一個可用于生產的工具,但可作為在我們的工具中使用 ORM 的相關步驟的說明性示例。
在 CLI 中集成 config 文件
Python INI config 語法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
[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 函數
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#!/usr/bin/env python import ConfigParser def readConfig( file = "config.ini" ): Config = ConfigParser.ConfigParser() Config.read( file ) sections = Config.sections() for machine in sections: #uncomment line below to see how this config file is parsed #print Config.items(machine) macAddr = Config.items(machine)[ 0 ][ 1 ] print machine, macAddr readConfig() |
該函數的輸出如下:
1
2
3
4
5
6
|
OSX 00 : 00 : 00 : 00 : 07 SOLARIS 00 : 00 : 00 : 00 : 04 AIX 00 : 00 : 00 : 00 : 02 REDHAT 00 : 00 : 00 : 00 : 05 UBUNTU 00 : 00 : 00 : 00 : 06 HPUX 00 : 00 : 00 : 00 : 03 |
我將剩下的問題作為練習留給讀者來解決。我接下來要做的是將該 config 文件集成到我的腳本中,這樣我就可以將我的 config 文件中記錄的機器庫存與出現在 ARP 緩存中的 MAC 地址的實際庫存進行比較。IP 地址或主機名只在跟蹤到計算機時才能發揮其作用,但是我們實現的工具對于跟蹤網絡上存在的計算機的硬件地址并確定它以前是否出現在網絡上可能非常有用。
結束語
我們首先通過編寫幾行代碼創建了一個非常簡單但功能強大的 Hello World 命令行工具。然后使用 Python 網絡庫創建了一個復雜的網絡工具。最后,我們繼續討論一些更高級的研究領域以饗讀者。在高級研究部分,我們討論了 subprocess 模塊、對象關系映射器的集成,最后討論了配置文件。
雖然并不為眾人所知,但任何具有 IT 背景的讀者都可以使用 Python 輕松地創建命令行工具。我希望本文能夠激勵您親自動手創建全新的命令行工具。