對話 UNIX: Squirrel--可移植的 shell 和腳本語言
1799 年,一名法國陸軍工程師取得了一項重大發現。不,不是鵝肝醬、卡門培爾奶酪、巴氏消毒法或沙特(Sartre)— 實際上,他發現了能夠破譯埃及古代象形文字的鑰匙 —— 羅塞塔石碑(參見 圖 1)。
圖 1. 羅塞塔石碑,1100 磅重,其上使用三國語言篆刻了稅收策略。碑文展示的是減免僧侶稅款的詔書。
這塊石碑制作于公元前 196 年,篆刻了對同一段文字的三種不同語言版本 — 分別是象形文字、通俗體文字(埃及草書)和希臘文字。通過對照翻譯,或在不同語言版本之間尋找對應的詞匯,羅塞塔石碑解讀出已經失傳已久的象形文字的含義。
換句話說,將羅塞塔石碑想像成 Babelfish。即使在公元前 196 年,就出現了使用一種以上的語言進行表達。
公元 2000 年末,軟件開發人員面對著一個相似的問題。有太多的語言和方法可以用來表達同一內容。即使對于命令行,也有許多類似的內容可供選擇,包括各種 shell 和不同的命令組合。
通常來講,多樣性是件好事,但是它也會讓人覺得害怕。應該選擇哪種解決方案?這種技術是否能夠跟上需求的變化?時間和精力方面的投入能否得到回報?這些編寫良好的代碼(或 Perl 代碼)是否會過時?更糟糕的是,是否需要針對其他環境轉換(重寫)所有內容?
如果您不希望局限于 Fish shell、Bash shell、Z shell、Windows operating system 的 cmd.exe 或其他一些 shell 腳本語言的特性,那么請嘗試使用 Squirrel Shell。Squirrel Shell 提供了一種高級的、面向對象的腳本語言,在 Unix、Linux、Mac OS X 和 Windows 系統上都可以良好地運行。您只需要編寫一次腳本,就可以在任意平臺上運行。
更妙的是,您需要做的工作非常簡單。
獲得 Squirrel
根據 GNU Public License version 3 (GPLv3) 的條款,Squirrel Shell 很容易獲得并且可以免費使用。最新的版本為 2008 年 10 月 11 日發布的 1.2.2。Squirrel Shell 的創建者和維護者是 Constantin "Dinosaur" Makshin。
Squirrel Shell 的下載頁面(參見 參考資料)提供了針對 32 位和 64 位 Windows 的源代碼和二進制代碼。如果您使用 Unix 或 Linux,請檢查發行版附帶的庫,尋找合適的二進制文件或從頭構建 Squirrel Shell。
從頭構建 Squirrel Shell 非常簡單。下載并提取源代碼 tarball 文件,放到源代碼目錄,然后使用非常典型的構建 shell,如 清單 1 所示。
清單 1. 從頭構建 Squirrel Shell
$ ./configure --with-pcre=system && make && sudo make install Checking CPU architecture...x86 Checking for install.../usr/bin/install ... Configuration has been completed successfully. Build for x86 CPU architecture Installation prefix: /usr/local Allow debugging: no Build static librarIEs Use system PCRE 6.7 library Install MIME information: auto Create symbolic link: no Compile C code with 'gcc' Compile C++ code with 'g++' Create static libraries with 'ar rc' Create executables and shared libraries with 'g++' Install files with 'install'
要查找與包有關的選項列表以進行配置,需在命令行中輸入 ./configure --help。
為方便起見,Squirrel Shell 打包了 Perl Compatible Regular Expression (PCRE) 庫的源代碼,這些內容在程序中被大量使用。如果系統缺少 PCRE,打包后的代碼可以使構建變得簡單快捷。然而,如果系統已經有了 PCRE,那么可以通過指定 --with-pcre=system 選項來使用它。另一種方法是指定 --with-pcre=auto 以鏈接到更新的系統庫或 Squirrel Shell 的副本。
構建的結果是得到一個新的二進制文件,名為 squirrelsh。假設此文件被安裝到 PATH 變量的某個目錄中,比如 /usr/local/bin,那么輸入 squirrelsh 以啟動該 shell。在命令行提示符下,輸入命令 printl(getenv("HOME")); 以輸出主目錄的路徑:
$ squirrelsh > printl( getenv( "HOME" ) ); /home/strike > exit();
Squirrel Shell 基于 Squirrel 編程語言(參見 參考資料 獲得更多信息的鏈接)。該語言類似于 C++,并且提供了非常類似于 Python 和 Ruby 等面向對象腳本語言的特性。Squirrel Shell 納入了 Squirrel 中的所有特性和數據類型,并添加了一些專門為常見 shell 腳本任務編寫的新功能,比如復制文件和讀取環境變量。
盡管 Squirrel Shell 的語法對于日常的命令行使用過于繁雜 —echo $HOME 是和 Squirrel Shell 的 printl( "~") 具有等效功能的 Bash 命令 — 但是它擁有出色的腳本。您只需要編寫一次,就可以到處運行,而不需要針對 Unix 和 Windows 分別編寫。正如 Dinosaur 這樣評價他的工作,“Squirrel Shell 主要是充當一個腳本翻譯器。
使用 Squirrel 編寫腳本
讓我們看一看一個 Squirrel Shell 腳本的示例。清單 2 展示了文件 listing2.nut,此腳本將遞歸地列出您的主目錄的內容。
清單 2. listing2.nut
#!/usr/bin/env squirrelsh function reveal( filedir ) { if ( !exist( filedir ) ) { return; } if ( filename( filedir ) == ".." || filename( filedir ) == "." ) { return; } if ( filetype( filedir ) == FILE ) { printl( filename( filedir, true ) ); return; } printl("Directory: " + filename( filedir, true) ); local names = readdir( filedir ); foreach( index, name in names ) { reveal( name ); } } local previous = getcwd(); chdir( "~" ); reveal( getcwd() ); chdir( previous ); exit( 0 );
按照規定,每個 shell 腳本的第一行將向操作系統表明要啟動哪個程序來解釋腳本。通常,這一行會顯示 #! /usr/bin/bash 或 #! /bin/zsh 以從某個位置啟動特定 shell 或解釋器。
#!/usr/bin/env squirrelsh 有一些不同。它啟動了一個特殊的程序 env,此程序又啟動 PATH 變量中找到的第一個 squirrelsh 實例。因此,可以修改 PATH 變量以支持某個程序的本地版本 — 即您自己的、修改后的 squirrelsh 副本,位于 $HOME/bin/squirrelsh — 而不要修改 shell 腳本的內容。
注意:這個技巧適用于所有解釋器。例如,#!/usr/bin/env ruby 將按照 PATH 設置的指示,調用您喜歡的 Ruby 版本。總之,如果計劃發布所編寫的任何 shell 腳本,在第一行中使用 #!/usr/bin/env application 表單,因為它的 “移植性 更強:它將運行用戶 在他/她的 PATH 變量中已經配置好的應用程序版本。
清單 2 的其余部分應該比較熟悉,至少對于方法是這樣。函數 reveal() 是遞歸的:
如果為 reveal() 傳遞一個無效的路徑或 “小圓點(.,當前目錄)或 “兩個小圓點(..,父目錄),那么遞歸將結束。
否則,如果參數 filedir 是一個文件,代碼將輸出其名稱并返回,并再一次停止進一步的遞歸。函數 filename() 可以接受一到兩個參數。如果只有一個參數,或者第二個參數為 false,那么將忽略擴展文件名。如果提供 true 作為第二個參數,將返回完整的文件名。
如果參數是一個目錄,代碼將輸出其名稱,然后掃描內容(不需要執行深度優先處理,因為目錄內容并沒有按特定的順序排列。下一個示例將改進輸出)。
需要注意一點:由于對 reveal() 的調用是同一個函數中的最后一條語句,Squirrel 虛擬機(VM)— 運行腳本代碼的引擎 — 可以通過稱為尾遞歸(tail recursion)的技術將遞歸改為迭代。實際上,尾遞歸消除了對遞歸使用調用棧的需要;因此,可以實現任意深度的遞歸并且可以避免棧溢出。
Squirrel 的語法相當簡單,因此使用這種語言編寫代碼非常快捷,特別是如果您曾經使用過 C、C++ 或任何更高級的語言編寫過代碼的話,這一點則體現得更充分。
最妙的是,這個 shell 代碼是可移植的。將它轉移到 Windows 機器上,在其上安裝 Squirrel Shell,然后就可以運行您的代碼。
改進表
與典型 shell 相比,Squirrel 的優秀特性之一就是它豐富的數據結構。如果數據可以進行良好地組織,那么即使是復雜的問題通常也能夠快速得到解決。Squirrel 提供了真正的對象、異構數組和關聯數組(在 Squirrel 中稱為 表)。
一個 Squirrel 表由一些 slot 或 (鍵-值)對組成。除 Null 以外的任何值都可以充當一個鍵;任何值都可以被分配給一個 slot。您將使用 “箭頭 操作符創建一個新的 slot(<-)。
讓我們對 清單 2 的代碼稍加改進,在將目錄轉變為任何子目錄之前展示它的內容。使用什么方法?使用一個本地表在單獨的 slot 中存放文件和子目錄,然后相應地處理兩個類別。清單 3 展示了新的代碼。
清單 3. 增強后的清單 2 將首先輸出目錄的內容,然后遞歸到子目錄
#!/usr/bin/env squirrelsh function reveal( filedir ) { local tally = {}; tally[FILE] <- []; tally[DIR] <- []; if ( !exist( filedir ) ) { return; } if ( filename( filedir ) == ".." || filename( filedir ) == "." ) { return; } local names = readdir( filedir ); foreach( index, name in names ) { tally[ filetype( name ) ].append( name ) ; } foreach( index, file in tally[FILE] ) { printl( file ); } foreach( index, dir in tally[DIR] ) { printl( filename( dir ) + "/" ); } foreach( index, dir in tally[DIR] ) { reveal( dir ); } } local entrIEs = readdir( (__argc >= 2) ? __argv[1] : "." ); exit( 0 );
在這里非常適合使用表這種數據結構。reveal() 中的表有兩個 slot:一個用于文件,另一個用于目錄。filetype( name ) 函數的返回值 — 常量 FILE 或常量 DIR — 將文件系統中的每一項整理到相應的 slot 中。
此外,每個 slot 是一個數組,由 tally[FILE] <- [] 和 tally[DIR] <- []; 這兩條語句創建。([] 是一個空數組)。由于 tally 是函數內的本地變量,它將在每次調用時重新創建并清空范圍,并且在每個調用被返回時自動銷毀。
數組函數 append( arg ) 將 arg 添加到數組的末尾,從而在此過程中形成了一個列表。在執行完 foreach( index, name in names ) 循環后,所有項都被添加到這兩個 slot 中其中一個的列表中。函數其余部分的代碼將輸出文件,接著輸出目錄,然后是遞歸。
當然,如果沒有命令行參數的話,shell 腳本的價值就沒有那么大了。特殊 Squirrel Shell 變量 __argc 和 __argv 分別以字符串數組形式包含命令行參數的計數和參數列表。根據約定,__argv[0] 始終都作為 shell 腳本的名稱;因此,如果 __argc 的值至少為 2,那么將提供額外的參數。為了簡單起見,這個腳本只處理第一個額外參數 argv[1]。
作為參考,清單 4 展示了一個 Ruby 腳本(作者為 Mr. Makshin),此腳本的功能與清單 3 相同。即使該腳本已像 Ruby 那樣簡潔,但它在簡潔性方面仍然遜色于 Squirrel Shell 代碼。
清單 4. 使用 Ruby 重新實現清單 3
!/usr/bin/ruby # List Directory contents. path = ARGV[0] == nil ? "." : ARGV[0].dup # Remove trailing slashes while path =~ //$/ path.chop! end entrIEs = Dir.open(path) for entry in entries unless entry == "." || entry == ".." filePath= "#{path}/#{entry}" fileStat = File.stat(filePath) if fileStat.directory? puts "dir : #{filePath}" elsif fileStat.file? puts "file: #{filePath}" end end end entries.close()
有關 Squirrel 語言的更多信息,請參閱 Squirrel Programming Language Reference(參見 參考資料 獲得鏈接)。
巧妙的是,Squirrel Shell 中的幾乎所有函數都去掉了底層操作系統的細節,因此您的代碼可以盡可能保持通用。例如,filename() 函數(在前兩個清單中使用)將引導路徑(leading path)從文件路徑名中分離 — 比如,將 /home/example/some/Directory/file.txt 簡化為 file.txt — 而不管您使用的是何種平臺。類似地,readdir() 和 filetype() 允許您不必了解真實的、底層操作和文件系統的圈套和陷阱。通常,普通的 shell 并不能提供這種抽象(較為高級的腳本語言則可以)。
其他有用的、獨立于平臺的功能包括 convpath() 和 run(),前者可以將路徑名轉換成本地路徑名格式,而后者可以調用另一個可執行文件。convpath() 函數可以執行雙向轉換,因此對于編寫跨平臺腳本非常有用。
正則表達式
Shell 腳本通常用于自動化系統管理和維護工作。實現這種自動化主要依靠正則表達式,它是用來查找、匹配和分解字符串的一組真正的象形文字。如前所述,Squirrel Shell 需要 PCRE 庫,這種庫在 Perl、PHP、Ruby 和其他許多解釋器和程序中都可找到。PCRE 是用于數據處理的重要武器。
盡管非常完整,Squirrel Shell 的正則表達式實現有一些不同,可能會令您想起 PHP 實現。要在 Squirrel Shell 中使用正則表達式,需要先定義正則表達式,對其進行編譯,進行比較,然后再迭代結果(如果有的話)。
清單 5 展示的示例程序演示了 Squirrel Shell 中的正則表達式(代碼由 Mr. Makshin 編寫并且得到使用許可)。
清單 5. 演示 Squirrel Shell 中的正則表達式
#!/usr/bin/env squirrelsh // Match a regular expression against text print("Text: "); local text = scan(); print("Pattern: "); local pattern = scan(); local re = regcompile(pattern); if (!re) { printl("Failed to compile regular expression - " + regerror()); exit(1); } local matches = regmatch(re, text); if (!matches) { printl("Failed to match regular expression - " + regerror()); regfree(re); exit(1); } regfree(re); printl("Matches found:"); foreach (match in matches) printl("t"" + substr(text, match[0], match[1]) + """);
在這里,scan() 從標準輸出中讀取一些文本和一個模式,但是并不包含通常用于確定正則表達式的起始和結束部分的前斜杠(/)字符。
對于一個模式,函數 reqgcompile() 將編譯此模式,這將提高匹配的速度。您可以對 reqgcompile() 函數使用一個標記以啟用或禁用區分大小寫的功能(等同于 PCRE /i 修飾符),并且可以使用另一個選項針對一行或多行進行匹配(等同于 PCRE /m 選項)。如果沒有對正則表達式執行編譯,那么所有匹配將失敗。
regmatch(re, text) 函數將比較正則表達式和文本,如果沒有匹配的話就生成 Null 值,否則生成一個由成對整數組成的數組(雙元素數組)。每一對中的第一個整數表示匹配的開始;第二個整數表示匹配結束。這解釋了最后一行代碼中 substr(text, match[0], match[1]) 的使用。
執行完比較后,可以迭代結果。如果在任何時候不再需要編譯后的正則表達式,則使用 regfree() 刪除它。還有一個 regfreeall() 函數可以處理所有已編譯表達式所持有的所有資源。
Squirrel Shell 的限制
在理想情況下,相同的編程邏輯將應用到 Unix、Linux 和 Windows 中,并且效率至少和以前一樣高,這樣程序員會更加高興。可惜操作系統各不相同,您經常需要為了某個特定系統而求助于定制代碼。
在這些情況下,無論是 Squirrel Shell 還是您都無法脫離平臺,Squirrel Shell 提供了一個方便的函數來探測操作系統,這樣代碼就可以適當的執行。
清單 6 展示了如何使用 platform() 函數作出決策。該函數始終返回一個值,但是該值可能是 unknown。
清單 6. platform() 函數生成操作系統類型
print( "Made by ... "); local platform = platform(); switch ( platform ) { case "linux": printl( "Linus." ); break; case "Macintosh": printl( "Steve." ); break; case "win32": case "win64": printl( "Bill." ); break; default: printl( "Unknown" ); }
您可以通過 Squirrel Shell 環境變量 PLATFORM 查找當前平臺的類型:
> printl( PLATFORM ); linux
環境變量 CPU_ARCH 生成處理器,shell 將針對該處理器進行編譯:
> printl( CPU_ARCH ); x86
結束語
Squirrel Shell 的其他函數將管理文件、處理環境和執行策略。實際上,它的三角學內置函數就有 20 余種。Version 2.0 目前正在規劃之中,并且將包含更多類、對 Unicode 的支持、改進的交互模式,以及一個模塊化的插件架構。
Squirrel Shell 并不算得上一種交互式 shell,但是這沒關系。在這方面已經出現了很多選擇。作為一種腳本運行程序,Squirrel Shell 要比其同類出色許多。其數據結構要比傳統 shell 更加強大,它的語法簡單易懂,其底層虛擬引擎支持從枚舉類型到線程等所有內容。Squirrel 引擎也很小巧,不超過 6000 行代碼。您甚至可以將完整的 Squirrel 嵌入到另一個應用程序中。
當您需要為兩個平臺編寫代碼時,請嘗試使用 Squirrel Shell!它使您能夠輕松編寫自己的代碼。