1 利用 Ruby 來黏著不同的外部程式
身為一個系統管理者,為什麼會迷上 Ruby 呢? 有管理過 unix 的管理者,會發現他每天必須寫許多 script 來結合不同的程式,完成無數的工作。 能有效把不同外部程式黏起來的語言, 才能成為系統管理者的瑞士刀。
作為一個好的黏著語言, 本身必須不利用外部程式語言便可以執行大部份的外部程式, 像要額外利用 C 來為 Ruby 寫 Binding 讓它引用 C 的函式庫, 便不在本文討論之列。
Table 1. Ruby 黏著外部程式的方法
外部程式種類 | 呼叫方法 | 呼叫語法 |
---|---|---|
可執行檔 | system, exec 或 `字元 | 遵照命令的說明組成命令列字串作為引數傳入 |
DLL | DL 模組 | 可以 Ruby 函數的語法呼叫, 但需手動指定函數名稱及函數引數及回傳值的資料型態。 |
Windows API | Win32API 物件 | 與 DL 類似,僅能用 |
OLE | WIN32OLE 物件 | OLE 檔案本身包含物件導向的資訊, 可以直接利用 Ruby 的物件語法來使用函式庫 |
1.1 呼叫外部程式
使用者與作業系統主要溝通的介面通常是命令列模式(Command mode), 並能透過命令列來通知作業系統去呼叫不同外部程式來完成所需的工作, 命令列語法通常如下:
Code 1. 命令列語法
Syntax: Command [parameters] Ex: notepad Rakefile
從Code 1. 命令列語法可以看到命令列的第 1 個字是命令名稱(Command),一般說來就是外部程式的檔名, 作業系統會從環境變數中,讀出程式載入路徑變數(Path), 再從路徑變數中記載的目錄中, 找出與命令名稱相同的執行檔並載入至記憶體中執行, 最後將命令列中的其餘字, 也就是傳給此命令的參數,傳給此執行檔。 上例就是 notepad 命令, 其後指定一個參數為 Rakefile, 此命令列表示用 notepad 這個程式來編輯 Rakefile 檔案。
Code 2. 呼叫 notepad Rakefile 指令
system("notepad Rakefile") system("notepad", "Rakefile")
Ruby 呼叫外部程式的方法是用 system, 並把命令列作為引數傳入, 如Code 2. 呼叫 notepad Rakefile 指令所示,
除了傳整個命令列,也可如第 2 行, 以 Ruby 函數的參數列語法, 來執行外部程式,其規定是第 1 個引數是命令, 第 2 個以後的引數便是傳給該命令的參數, 如同中命令列用空格區分的參數。
1.2 system 的傳回值
作業系統所呼叫的程式執行完畢後, 通常會把執行結果狀態用一個整數表示, 並寫到系統的 EXIT 變數, 命令若正常執行完成, 則 EXIT 應該為 0,反之則代表程式執行有異常, ruby 的 system 則是 EXIT 為 0 時,傳回 true, 反之則傳回 false 表示程式執行異常。
Code 3. 判斷 Widows 的版本
if system('setx /?' ) puts "VISTA" else puts "WINXP" end
我們來看一個應用的例子, Code 3. 判斷 Widows 的版本的 script 是用來判斷目前的 windows 版本,當執行此 script 時, 在最後會印出 VISTA 或 WINXP 字串, 來讓使用者知道目前 Windows 作業系統的版本,
此 script 是利用作業系統是 Vista 時, 才會有 setx 指令, 而 WinXP 並未有此指令。 是故在 WinXP 下,此命令會找不到執行檔, 而在 Vista 則可執行成功,system 會傳回 true。 反之在 WinXP 執行 system 會傳回 false。 藉此差異便能用來判斷 windows 的版本。
1.3 呼叫 Windows 的內建命令
Code 4. 直接呼叫 Windows 的內建命令
system 'dir' # => false
在 Windows 下,我們不可以直接呼叫內建命令, 如Code 4. 直接呼叫 Windows 的內建命令會產生錯誤。
Code 5. 呼叫 Windows 的內建命令
system 'cmd /c dir' # => true
取而代之要在內建指令前加上 cmd /c 來執行, 如Code 5. 呼叫 Windows 的內建命令所示。
1.4 system 與 exec 比較
1.4.1 程式與程序
system 及 exec 方法在語法及作用上都是類似的, 要暸解其間差別, 首先要先暸解程序(Process)與程式(Program)的差別。
以前的電腦通常製造出來只能達到當初設計的功能, 像是二戰時的軍用加密電腦,就只能作加密的功能, 而現代的電腦通常能執行大部份的一般功能, 真正的功能還需要由程式實現, 這指的是我們可以改變程式後, 電腦就能執行不同的功能,而不用重作一台電腦。
CPU 是一台電腦神經中樞, 可以接受指令的輸入, 解譯它並執行它。它能執行的指令集合,稱為指令集, 一般我們常見的指令集為 X86 架構。
程式就是一連串 CPU 指令,但不享有執行資源, 必須等候作業系統將其載入到記憶體, 並配給它程式計數器(Program counter)、暫存器及 CPU 時間, 才算真正執行。 此時可以執行的程式稱為程序(Process)。
程式計數器用來告知 CPU 程序已執行到那行指令了, CPU 可由程式計數器去取得下一個應該執行的指令, 是故能可執行程式必須要有程式計數器。
Code 6. exec 的例子
exec 'cmd /c dir' # => true puts "此命令永遠不會執行"
system 及 exec 的差異就在 system 執行其傳入的命令時, 是先複製本身程序的執行環境, 建立一個子程序來執行外部程式, 外部程式結束時會再回到呼叫它的 ruby 程式。 而 exec 是用本身執行環境來執行外部程式, 所以外部程式結束時,不會再回到呼叫它的 ruby 程式, 如Code 6. exec 的例子所示,永遠不會執行到最後一句命令。
1.5 命令輸出替換子
若把一串命令用倒引號字元”`“括起來, 則會表示這個命令執行後所輸出字串。
Code 7. 雙重解壓縮
m = `unzip -o attch1.zip -d tmpdir` m =~ /.*\.rar/ tf = $& unrar tf
我以一個之前解決的問題為例, 某機關網站的公文系統附件一律是用 zip 格式壓縮, 但其下屬機關有個例行性的資料更新是用 rar 壓縮, 所以上傳至機關網站的公文系統,又會使用 zip 格式再壓縮一次, 上傳至公文系統的附檔檔名統一為 attch1.zip, 這部份讓我們可以寫下自動化程式, 但是第二層的 rar 其檔名是不統一的, 因此利用 unzip 命令會輸出所解壓檔案的檔名, 並且篩選其中附檔名為 rar 的字串, 便能得知 rar 的檔名為何, 再對其作 unrar 的動作。 如Code 7. 雙重解壓縮所示, 我們利用 m 來接受倒引號所括住的 unzip 命令的輸出, 並使用正規表示式來得到 unzip 出來的 rar 檔名, 最後在把檔名傳給 unrar,完成整個程序的自動化。
1.6 隱藏命令的輸出
通常系統管理的 script 常會執行一連串命令列, 但是呼叫的命令列都會把它的輸出寫到標準輸出及標準錯誤, 這些額外的輸出會混雜在原本 script 的輸出中, 使管理員必須費時找出他想要的訊息。
Example 1. 在 Vista 環境的執行結果
D:\moneylog\doc\rubysysadm>ruby winver1.rb SetX 的作用方式有三種: 語法 1: SETX [/S system [/U [domain\]user [/P [password]]]] var value [/M] 語法 2: SETX [/S system [/U [domain\]user [/P [password]]]] var /K regpath [/M] 語法 3: SETX [/S system [/U [domain\]user [/P [password]]]] /F file {var {/A x,y | /R x,y string}[/M] | /X} [/D delimiters] 描述: 建立或修改使用者或系統環境的 環境變數。可以根據引數、登錄機碼或 檔案輸入來設定變數。 參數清單: /S system 要連線的遠端系統。 /U [domain\]user 指定執行命令的使用者。 /P [password] 指定使用者密碼。 如果省略,會出現密碼輸入要求。 var 指定要設定的環境變數。 value 指定值委派給 環境變數。 /K regpath 指定變數應該根據登錄 機碼的資訊來設定。 路徑的格式應該指定為 hive\key\...\value。例如, HKEY_LOCAL_MACHINE\System\CurrentControlSet\ Control\TimeZoneInformation\StandardName. /F file 指定要使用的文字檔案的 檔名。 /A x,y 指定絕對檔案協調 (行 X, 項目 Y) 檔案的搜尋 參數。 /R x,y string 指定檔案內的相對協調的 "string" 作為搜尋參數。 /M 指定變數應該設定在 整個系統 (HKEY_LOCAL_MACHINE) 環境中。預設是設定 HKEY_CURRENT_USER 環境之下 的變數。 /X 顯示有對應協調的檔案內容。 /D delimiters 指定額外的分隔字元,例如 "," 或 "\"。內建的分隔字元有空格、 tab 鍵和換行符號。任何 ASCII 字元都可以用做額外的分隔 字元。分隔字元的字數上限, 包括內建分隔字元在內是 15 個字。 /? 顯示這個說明訊息。 注意: 1) SETX 將變數寫入登錄中的主要環境。 2) 在本機系統上,由這個工具建立或修改的變數 可以在未來的命令視窗中使用,但是 不可以在目前的 CMD.exe 命令視窗中使用。 3) 在遠端系統上,由這個工具建立或修改的變數 在下次登入工作階段將可使用。 4) 有效的 RegKey 資料類型有 REG_DWORD、REG_EXPAND_SZ、 REG_SZ 和 REG_MULTI_SZ。 5) 得到支援的 hive 有 HKEY_LOCAL_MACHINE(HKLM) 和 HKEY_CURRENT_USER (HKCU)。 6) 分隔字元會區分大小寫。 7) REG_DWORD 值將以小數格式從登錄解壓縮。 範例: SETX MACHINE COMPAQ SETX MACHINE "COMPAQ COMPUTER" /M SETX MYPATH "%PATH%" SETX MYPATH ~PATH~ SETX /S system /U user /P password MACHINE COMPAQ SETX /S system /U user /P password MYPATH ^%PATH^% SETX TZONE /K HKEY_LOCAL_MACHINE\System\CurrentControlSet\ Control\TimeZoneInformation\StandardName SETX BUILD /K "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\CurrentBuildNumber" /M SETX /S system /U user /P password TZONE /K HKEY_LOCAL_MACHINE\ System\CurrentControlSet\Control\TimeZoneInformation\ StandardName SETX /S system /U user /P password BUILD /K "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\ CurrentVersion\CurrentBuildNumber" /M SETX /F ipconfig.out /X SETX IPADDR /F ipconfig.out /A 5,11 SETX OCTET1 /F ipconfig.out /A 5,3 /D "#$*." SETX IPGATEWAY /F ipconfig.out /R 0,7 Gateway SETX /S system /U user /P password /F c:\ipconfig.out /X VISTA
以Code 3. 判斷 Widows 的版本為例, 在 Vista 環境下時,先會印出 setx /? 的說明訊息, 最後才印出 VISTA 這個字串, 其雜亂的輸出如Example 1. 在 Vista 環境的執行結果所示。 而在 winxp 環境下時,先會印出 “環境變數 x/? 未定義” 的錯誤訊息, 才印出 WINXP 字串。
Code 8. 判斷 Widows 的乾淨輸出版本
if system('setx /? >nul 2>nul' ) puts "VISTA" else puts "WINXP" end
為了有乾淨的輸出,傳給 system 的指令字串, 我們必須清除外部命令列本身輸出, 通常的作法是來把標準輸出及錯誤輸出重導到黑洞檔案, 因為寫進黑洞檔案的資料都會消失。
在 Windows 環境下,nul 表示黑洞檔案,
nul 2>nul 分別表示把標準輸出及錯誤輸出重導到黑洞檔案,
作了這些改變, 使Code 8. 判斷 Widows 的乾淨輸出版本便只會印出 VISTA 或 WINXP 這個乾淨的字串。
若妳是在 unix 環境, 黑洞檔案被視為一個裝置,放置在 /dev/null 中, 我們可以在指令尾加上 > /dev/null 2> /dev/null, 來達到同樣的效果。
1.7 呼叫動態共用函式庫的函數
現代的作業系統都提供動態連結函式庫(DLL, Dynamic LinkLibrary) 讓程序呼叫可執行程式碼中部分的函式。 DLL 是扮演共用程式庫功能的可執行檔, 裡面包含一個或多個已編譯、連結的函式, 並且儲存在與呼叫它們的程序不同的地方。 DLL 可以讓多個應用程式可以同時存取記憶體中 DLL 單一複本的內容,
動態連結與靜態連結的不同處在於, 動態連結的可執行模組只包含在執行階段時用來找出 DLL 函式可執行程式碼的資訊。 但靜態連結的可執行檔包含所有參考函式的程式碼。
動態連結可提供許多優點像是節省記憶體、節省磁碟空間、 較容易升級、提供擴充程式庫機制。
Code 9.
require 'dl/import' require 'scanf' dllex = DL.dlopen 'dll.dll' init_list = dllex['init_list', 'PI'] printf "Enter the number of a list:" num = $stdin.scanf("%d").first r, rs = init_list.call num until r.nil? r.struct! 'IP', :val, :next print "->#{r[:val]}" r = r[:next] end puts
Ruby/DL 用 Handle 來表示要使用的 DLL, 是故使用 DLL 必須先建立一個 Handle 物件來表示要使用的 DLL, 常用 DL.dlopen 方法來建立 Handle 物件, 此方法接受 dll 的檔案路徑作為參數, 例如Code 9.
DLL 包含一個符號表使函式名稱能來查詢指定函式在記憶體中的位置, 但是 DLL 中並沒有函式原型, 也就是函式的引數及傳回值的資料型態, 是故必須手動指定函式原型。
DLL 內的函式是用 Symbol 物件表示, Symbol 含有函式名稱及函式原型。 我們主要用 Handle# 方法來取得表示函式的 Symbol 物件, 其語法如下:
Code 10. Handle#
Handle#[function, prototype]
其中 function 引數為函式名稱,prototype 為函式原型。 如Code 9.
Table 2. 資料類型代碼表
代碼 | 對應的 C 類型 |
---|---|
C | char |
c | char* |
H | short |
h | short* |
I | int |
i | int* |
L | long |
l | long* |
f | float |
F | float* |
D | double |
d | double* |
S | const char* |
s | char* |
A | const type[] |
a | type[] |
P | void* |
p | void* |
0 | void function() |
函式原型為一個字串,每個字元表示一種資料類型, 如Table 2. 資料類型代碼表所式, 字串的第一個字元指定回傳值的資料型態, 其餘字元依序表示傳入函式引數的資料型態, ‘PI’ 字串表示函式回傳資料型態代碼為 P, 表示回傳參數為一個指標, I 表示函式具有一個引數且資料型態為 int。
Code 11. dll.h
#ifndef DLL_H__ #define DLL_H__ typedef struct list { int val; struct list* next; }*node; node init_list(int num); #endif
Code 12. dll.c
#include <stdio.h> #include <string.h> #include <stdlib.h> #include "dll.h" node init_list(int num) { node head, curr; head = NULL; int i; for(i=1;i<=num;i++) { curr = (node)malloc(sizeof(node)); curr->val = i; curr->next = head; head = curr; } return head; }
Code 11. dll.h及Code 12. dll.c定義一個範例 dll, 裡頭定義一個稱為 node 的 struct 資料型態來實作單向連結串列, 也定義一個函數
Code 13.
#include <stdio.h> #include "dll.h" int main() { int num; printf("Enter the number of a list:"); scanf("%d", &num); node head, curr; curr = NULL; head = init_list(num); curr = head; while(curr) { printf("->%d", curr->val); curr = curr->next; } puts(""); }
之後我們用 DL::Handle# 方法來取得表示 DLL 函數的 DL::Symbol 物件, 如
建立 DL::Symbol 物件後,便能用 call 方法來呼叫此函數, call 方法可以接受用引數資料類型字串定義的參數串, 如上例我們傳一個整數給
C 指標變數用 PtrData 物件來表示 像上述的
Code 14. PtrData#struct!
PtrData#struct! type_specifier, *keys
語法如Code 14. PtrData#struct!示, 第一個參數為資料型態字串, 而其餘參數為用來存取各個結構內成員的鍵值。
由於 node 類型包含一個 int 及一個指標, 故資料型態字串為 ‘IP’, 並指定 :val 及 :next 符號為存取上述 2 個成員的鍵值。
有了上述知識後, 如Code 9.
1.8 呼叫 Windows 的 API
在 Windows 環境中, 如果支援 OLE 介面的函式庫仍不能解決您的問題的話, 那麼只好利用更低階的 WINAPI 函式。 Ruby 主要利用 Win32API 物件來存取到 WINAPI 的函式。
由於 WINAPI 主要是包裝在 DLL 中, 要存取 DLL 中的函式,
Code 15. 取得游標位置
require 'Win32API' result = "0"*8 # Eight bytes (enough for two longs) getCursorXY = Win32API.new("user32","GetCursorPos",["P"],"V") getCursorXY.call(result) x, y = result.unpack("LL") # Two longs puts "The cursor is in (#{x}, #{y})."
為一個印出目前游標位置的小程式, 它主要是利用 GetCursorPos 這個 WINAPI 函式來取得目前游標位置。 要呼叫 WINAPI 函式首先要寫 require ‘Win32API’ 來載入 Win32API 這個 Ruby 函式庫來存取 WINAPI 函式, 每個 WINAPI 函式在 Ruby 中會使用一個 Win32API 物件表示, 由於 WINAPI 不像 OLE 有物件的結構,而是低階的 C 語言資料結構, 且 Ruby 語言本身函數參數本身並無型態的概念, 所以在呼叫 WINAPI 還必須指定參數的資料型態。
1.9 以 OLE 介面呼叫外部程式
OLE 是呼叫 Windows 外部應用程式的常用介面, 微軟當初是為了能讓不同的程式語言來控制其應用程式, 並完成應用才發展這個介面。 目前會利用 OLE 主要是控制 MS Office 系統軟體。
Code 16. 利用 OLE 控制 Word
require "win32ole" if RUBY_PLATFORM =~ /win/ begin word = WIN32OLE.connect("word.application") rescue word = WIN32OLE.new("word.application") word.documents.add end word.visible = true selection = word.selection selection.typeText("Hello WIN32OLE!")
要使用 OLE 連接應用程式,首先要載入 win32ole 函式庫, 由於這個函式庫只在 windows 環境下能作用, 若程式在其它環境,載入命令會發生錯誤, 如Code 16. 利用 OLE 控制 Word所示, 我們在 require 後面加上 if 修飾字來過濾只在 windows 環境, 才載入 win32ole 函式庫。
然後我們利用 connect 來連結 word 應用程式, 連接 word 應用程式的 OLE 名稱為 word.application, 把它傳給 connect 方法, 之後我們便能用 connect 所傳回的 WIN32OLE 物件來控制 word, 此介面會自動把 word 所提供的物件轉換成 ruby 的物件, 如我們可以取得 word 的 selection 物件, 並利用它的 typeText 方法在目前的 word 打出 Hello WINOLE! 字串。