對話 UNIX: Squirrel--可移植的 shell 和腳本語言

火星人 @ 2014-03-12 , reply:0


  
如果您不滿足於特定的 shell 僅能在某個特殊平台上運行,那麼可嘗試使用 Squirrel Shell。Squirrel Shell 提供了一種高級的、面向對象的腳本語言,在 UNIX®、Linux®、Mac OS X™ 和 Windows® 系統上都可以良好地運行。只需要編寫一次腳本,就可以在多個平台上運行。

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!它使您能夠輕鬆編寫自己的代碼。(責任編輯:A6)






[火星人 via ] 對話 UNIX: Squirrel--可移植的 shell 和腳本語言已經有253次圍觀

http://www.coctec.com/docs/linux/show-post-68882.html