用 Swift 來寫命令列程式
這是探索 Swift 寫 Linux 程式的系列文章中的一篇。
在上一個例子中,我們通過組合使用 popen
和 wget
命令來呼叫自然語言翻譯服務,來實現像 Google 翻譯那樣的翻譯功能。本文的程式會基於之前我們已經完成的工作來進行。但與之前每次執行都只能翻譯一句話所不同的是,這次我們要實現一個具備互動功能的 shell
程式,來翻譯在控制檯輸入的每一句話。像下面的截圖一樣:
翻譯程式會顯示它接受什麼語言(源語言)以及翻譯的目標語言。比如:
en->es
英語翻譯為西班牙語es->it
西班牙語翻譯為義大利語it->ru
義大利語翻譯為俄羅斯語
翻譯程式預設是 en->es
,並提供了兩個命令:to
和 from
來實現語言的切換。比如,輸入 to es
將會把翻譯的目標語言設定為西班牙語。輸入 quit
可以退出程式。
如果使用者輸入的字串不是命令的話,翻譯程式會把輸入逐字地傳送到翻譯的 web 服務。然後把返回的結果列印出來。
需要注意的幾點
如果你是系統或者運維程式設計師,並且以前也沒接觸過 Swift 的話,下面是一些你在程式碼裡需要注意的事情。我想你會發現 Swift 為兩種型別的工程師都提供了很多有用的特性,並且會成為 Linux 開發體系中一股很受歡迎的新力量。
let variable = value
常量賦值- 元組(tuples)
switch-case
支援字串switch-case
使用時必須包含所有情況(邏輯完備性)- 計算型屬性
import Glibc
可以匯入標準的 C 函式guard
語句- 可以使用
NSThread
和NSNotificationCenter
這些蘋果的 Foundation 框架中的類。 - 在不同的執行緒或不同的物件裡通過傳送訊息來觸發特定程式碼的執行
程式設計
我們的翻譯程式可以拆分成一個主程式、兩個類以及一個 globals.swift
檔案。如果你打算跟著做,那你應該使用 Swift 的包管理器,然後調整你的目錄結構為下面這樣:
translator/Sources/main.swift /Sources/CommandInterpreter.swift /Sources/... /Package.swift
main.swift
檔案是 Swift 應用程式的入口並且應該是唯一一個包含可執行程式碼的檔案(在這裡,像「變數賦值」,或者「宣告一個類」不屬於「可執行的程式碼」)。
main.swift
:
import Foundation import Glibc let interpreter = CommandInterpreter() let translator = Translator() // Listen for events to translate nc.addObserverForName(INPUT_NOTIFICATION, object:nil, queue:nil) { (_) in let tc = translationCommand translator.translate(tc.text, from:tc.from, to:tc.to){ translation, error in guard error == nil && translation != nil else { print("Translation failure: \(error!.code)") return } print(translation!) } } interpreter.start() select(0, nil, nil, nil, nil)
上面的程式碼表示我們的程式不接受命令列引數。具體的流程說明:
- 分別建立
CommandInterpreter
和Translator
類的例項 - 為
InputNotification
通知新增觀察者(這裡用到的常量INPUT_NOTIFICATION
常量定義在globals.swift
) - 新增當收到通知的時候要執行的程式碼
- 呼叫
Interpreter
類例項的start
方法 - 呼叫
select
來實現當程式有其他執行緒在執行的時候,鎖定主執行緒。(譯註:也就是防止主執行緒提前結束)
CommandInterpreter 類
CommandInterpreter
類主要負責從終端讀入輸入的字串,並且分析輸入的型別並分別進行處理。考慮到你可能剛接觸 Swift,我在程式碼裡對涉及到語言特性的地方進行了註釋。
// Import statements import Foundation import Glibc // Enumerations enum CommandType { case None case Translate case SetFrom case SetTo case Quit } // Structs struct Command { var type:CommandType var data:String } // Classes class CommandInterpreter { // Read-only computed property var prompt:String { return "\(translationCommand.from)->\(translationCommand.to)" } // Class constant let delim:Character = "\n" init() { } func start() { let readThread = NSThread(){ var input:String = "" print("To set input language, type 'from LANG'") print("To set output language, type 'to LANG'") print("Type 'quit' to exit") self.displayPrompt() while true { let c = Character(UnicodeScalar(UInt32(fgetc(stdin)))) if c == self.delim { let command = self.parseInput(input) self.doCommand(command) input = "" // Clear input self.displayPrompt() } else { input.append(c) } } } readThread.start() } func displayPrompt() { print("\(self.prompt): ", terminator:"") } func parseInput(input:String) -> Command { var commandType:CommandType var commandData:String = "" // Splitting a string let tokens = input.characters.split{$0 == " "}.map(String.init) // guard statement to validate that there are tokens guard tokens.count > 0 else { return Command(type:CommandType.None, data:"") } switch tokens[0] { case "quit": commandType = .Quit case "from": commandType = .SetFrom commandData = tokens[1] case "to": commandType = .SetTo commandData = tokens[1] default: commandType = .Translate commandData = input } return Command(type:commandType,data:commandData) } func doCommand(command:Command) { switch command.type { case .Quit: exit(0) case .SetFrom: translationCommand.from = command.data case .SetTo: translationCommand.to = command.data case .Translate: translationCommand.text = command.data nc.postNotificationName(INPUT_NOTIFICATION, object:nil) case .None: break } } }
CommandInterpreter
類的實現邏輯非常直觀。當 start
函式被呼叫的時候,通過 NSThread
來建立一個執行緒,執行緒中再通過 blockfgetc
的回撥引數 stdin
來獲取終端的輸入。當遇到換行符 RETURN
(使用者按了回車)後,輸入的字串會被解析並對映成一個 Command
物件。然後傳遞給 doCommand
函式進行剩下的處理。
我們的 doCommand
函式就是一個簡單的 switch-case
語句。對於 .Quit
命令則就簡單呼叫 exit(0)
來終止程式。.SetFrom
和 .SetTo
命令的功能是顯而易見的。當遇到 .Translate
命令時,Foundation 的訊息系統就派上用場了。doCommand
函式自己並不完成任何的翻譯功能,它只是簡單的傳送一個應用程式級別的訊息,也就是InputNotification
。任何監聽這個訊息的程式碼都會被呼叫(比如我們之前的主執行緒):
// Listen for events to translate nc.addObserverForName(INPUT_NOTIFICATION, object:nil, queue:nil) { (_) in let tc = translationCommand translator.translate(tc.text, from:tc.from, to:tc.to){ translation, error in guard error == nil && translation != nil else { print("Translation failure: \(error!.code)") return } print(translation!) } }
我在這篇文章中提到,在對 NSNotification
的 userInfo
字典做型別轉換時會有一個 SILGen 的閃退 crash,在這裡我們用一個叫做 translationCommand
的全域性變數來繞過這個 crash。在這段程式碼裡:
- 為了程式碼的簡潔,把
translationCommand
的內容賦值給tc
- 呼叫
Translator
物件的translate
方法,並傳入相關的引數 - 實現翻譯完成後的回撥
- 用一個 Swift 漂亮的
guard
語句來檢測是否有錯並返回 - 列印出翻譯的文字
Translator
Translator
類最開始是在這篇文章中介紹的,我們在這裡直接重用:
import Glibc import Foundation import CcURL import CJSONC class Translator { let BUFSIZE = 1024 init() { } func translate(text:String, from:String, to:String, completion:(translation:String?, error:NSError?) -> Void) { let curl = curl_easy_init() guard curl != nil else { completion(translation:nil, error:NSError(domain:"translator", code:1, userInfo:nil)) return } let escapedText = curl_easy_escape(curl, text, Int32(strlen(text))) guard escapedText != nil else { completion(translation:nil, error:NSError(domain:"translator", code:2, userInfo:nil)) return } let langPair = from + "%7c" + to let wgetCommand = "wget -qO- http://api.mymemory.translated.net/get\\?q\\=" + String.fromCString(escapedText)! + "\\&langpair\\=" + langPair let pp = popen(wgetCommand, "r") var buf = [CChar](count:BUFSIZE, repeatedValue:CChar(0)) var response:String = "" while fgets(&buf, Int32(BUFSIZE), pp) != nil { response = response + String.fromCString(buf)! } let translation = getTranslatedText(response) guard translation.error == nil else { completion(translation:nil, error:translation.error) return } completion(translation:translation.translation, error:nil) } private func getTranslatedText(jsonString:String) -> (error:NSError?, translation:String?) { let obj = json_tokener_parse(jsonString) guard obj != nil else { return (NSError(domain:"translator", code:3, userInfo:nil), nil) } let responseData = json_object_object_get(obj, "responseData") guard responseData != nil else { return (NSError(domain:"translator", code:3, userInfo:nil), nil) } let translatedTextObj = json_object_object_get(responseData, "translatedText") guard translatedTextObj != nil else { return (NSError(domain:"translator", code:3, userInfo:nil), nil) } let translatedTextStr = json_object_get_string(translatedTextObj) return (nil, String.fromCString(translatedTextStr)!) } }
整合各個部分
要把上面介紹的元件結合到一起,我們還需要建立額外的兩個檔案:globals.swift
和 Package.swift
。
globals.swift
:
import Foundation let INPUT_NOTIFICATION = "InputNotification" let nc = NSNotificationCenter.defaultCenter() struct TranslationCommand { var from:String var to:String var text:String } var translationCommand:TranslationCommand = TranslationCommand(from:"en", to:"es", text:"")
Package.swift
:
import PackageDescription let package = Package( name: "translator", dependencies: [ .Package(url: "https://github.com/iachievedit/CJSONC", majorVersion: 1), .Package(url: "https://github.com/PureSwift/CcURL", majorVersion: 1) ] )
如果一切都配置正確的話,最後執行 swift build
,一個極具特色的翻譯程式就完成了。
swift build Cloning https://github.com/iachievedit/CJSONC Using version 1.0.0 of package CJSONC Cloning https://github.com/PureSwift/CcURL Using version 1.0.0 of package CcURL Compiling Swift Module 'translator' (4 sources) Linking Executable: .build/debug/translator
如果你打算從現成的程式碼開始學習,可以從 Github 上獲取本站的程式碼,然後找到 cmdline_translator
目錄。
試試自己動手
現在的翻譯程式還有很多可以優化的地方。下面是一個你可以嘗試的列表:
- 接受命令列引數來設定預設的源語言和目標語言
- 接受命令列引數來實現非互動模式
- 新增
swap
命令來交換源語言和目標語言 - 新增
help
命令 - 整合
from
命令和to
命令。實現一行可以同時設定兩者, 比如from en to es
- 現在當輸入
from
命令和to
命令時,沒有同時輸入對應的語言時會崩潰,修復這個BUG - 實現對轉義符
\
的處理,實現程式的“命令”也可以被翻譯(比如退出命令:quit) - 通過
localizedDescription
對錯誤提示新增本地化的支援 - 在
Translator
類中實現但有錯誤發生時,通過throws
來處理異常
結束語
我從來不掩飾我是一個狂熱的 Swift 愛好者,我堅信它很可能既能像 Perl、Python 和 Ruby 這樣語言一樣出色的完成運維工作,也能像 C、C++ 和 Java 一樣出色的完成系統程式設計的任務。我知道現在和那些個單檔案指令碼語言相比,Swift 比較蛋疼的一點就是必須得編譯成二進位制檔案。我真誠的希望這一點能夠改善,這樣我就能不再關注語言層面的東西而是去做一些新,酷酷的東西。一些朋友已經在 Swift 的郵件列表中討論這一點,具體可以看這個帖子。
相關文章
- 編寫友好的命令列應用程式命令列
- 用nodejs寫一個命令列應用-前言NodeJS命令列
- 用 nodejs 寫一個命令列工具 :建立 react 元件的命令列工具NodeJS命令列React元件
- 如何寫一個 GNU 風格的命令列程式命令列
- 使用瀏覽器命令列編寫JavaScript程式碼瀏覽器命令列JavaScript
- Perl 作為命令列實用程式(轉)命令列
- 如何書寫Openstack命令列命令列
- 如果用程式語言來寫作文
- 用 PHP 寫一個命令列音樂下載器PHP命令列
- 用 symfony/console 元件寫命令列指令碼元件命令列指令碼
- 用 Swift 寫伺服器端Swift伺服器
- 在命令列開發 Android 應用程式命令列Android
- 開發 Linux 命令列實用程式(轉)Linux命令列
- 用 Swift 來刷 leet code 吧Swift
- 命令列寫複雜語句命令列
- 用Swift寫服務端 — Perfect框架Swift服務端框架
- 寫給iOS程式設計師的命令列使用祕籍iOS程式設計師命令列
- 寫一段java程式來執行linux命令JavaLinux
- 用nodejs寫一個命令列應用-package.json介紹NodeJS命令列PackageJSON
- Sublime 編寫編譯 swift程式碼編譯Swift
- 用Node.js寫的看股票的命令列小工具Node.js命令列
- Dart Pub Global 建立命令列應用程式(Windows)Dart命令列Windows
- 在命令列用使用有道翻譯,python寫一個命令列有道詞典命令列Python
- 用寫程式碼的方式來整理知識
- 1、Shell命令列書寫規則命令列
- linux命令列大小寫轉換Linux命令列
- LINUX命令列書寫規則Linux命令列
- swift 陣列Swift陣列
- Swift,陣列Swift陣列
- Swift,列舉Swift
- 我的小專欄《 Swift 遊戲開發》開始創作啦~一起來用 Swift 寫遊戲吧!Swift遊戲開發
- 從0開始用python寫一個命令列小遊戲(十)Python命令列遊戲
- 從0開始用python寫一個命令列小遊戲(二)Python命令列遊戲
- 從0開始用python寫一個命令列小遊戲(六)Python命令列遊戲
- 初學練習,用Perl寫的命令列五子棋命令列
- 編寫高效能的 Swift 程式碼Swift
- 【譯】如何使用PHP快速構建命令列應用程式PHP命令列
- 未來程式設計趨勢的12個猜想 命令列永存...程式設計命令列