用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 來創建一個線程,線程中再通過 block fgetc 的回調參數 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 {
- responseresponse = 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
試試自己動手
現在的翻譯程序還有很多可以優化的地方。下面是一個你可以嘗試的列表:
- 接受命令行參數來設置默認的源語言和目標語言
- 接受命令行參數來實現非交互模式
- 添加 swap 命令來交換源語言和目標語言
- 添加 help 命令
- 整合 from 命令和 to 命令。實現一行可以同時設置兩者, 比如 from en to es
- 現在當輸入 from 命令和 to 命令時,沒有同時輸入對應的語言時會崩潰,修復這個BUG
- 實現對轉義符 \ 的處理,實現程序的“命令”也可以被翻譯(比如退出命令:quit)
- 通過 localizedDescription 對錯誤提示添加本地化的支持
- 在 Translator 類中實現但有錯誤發生時,通過 throws 來處理異常
結束語
試試自己動手
現在的翻譯程序還有很多可以優化的地方。下面是一個你可以嘗試的列表:
- 接受命令行參數來設置默認的源語言和目標語言
- 接受命令行參數來實現非交互模式
- 添加
swap
命令來交換源語言和目標語言 - 添加
help
命令 - 整合
from
命令和to
命令。實現一行可以同時設置兩者, 比如from en to es
- 現在當輸入
from
命令和to
命令時,沒有同時輸入對應的語言時會崩潰,修復這個BUG - 實現對轉義符
\
的處理,實現程序的“命令”也可以被翻譯(比如退出命令:quit) - 通過
localizedDescription
對錯誤提示添加本地化的支持 - 在
Translator
類中實現但有錯誤發生時,通過throws
來處理異常
結束語
我從來不掩飾我是一個狂熱的 Swift 愛好者,我堅信它很可能既能像 Perl、Python 和 Ruby 這樣語言一樣出色的完成運維工作,也能像 C、C++ 和 Java 一樣出色的完成系統編程的任務。我知道現在和那些個單文件腳本語言相比,Swift 比較蛋疼的一點就是必須得編譯成二進制文件。我真誠的希望這一點能夠改善,這樣我就能不再關注語言層面的東西而是去做一些新,酷酷的東西。
我真誠的希望這一點能夠改善,這樣我就能不再關注語言層面的東西而是去做一些新,酷酷的東西。