用Swift來寫命令行程序
這是探索 Swift 寫 Linux 程序的系列文章中的一篇。
在上一個(gè)例子中,我們通過組合使用 popen 和 wget 命令來調(diào)用 自然語言翻譯服務(wù) ,來實(shí)現(xiàn)像 Google 翻譯 那樣的翻譯功能。本文的程序會(huì)基于之前我們已經(jīng)完成的工作來進(jìn)行。但與之前每次執(zhí)行都只能翻譯一句話所不同的是,這次我們要實(shí)現(xiàn)一個(gè)具備交互功能的 shell 程序,來翻譯在控制臺(tái)輸入的每一句話。像下面的截圖一樣:
翻譯程序會(huì)顯示它接受什么語言(源語言)以及翻譯的目標(biāo)語言。比如:
en->es 英語翻譯為西班牙語
es->it 西班牙語翻譯為意大利語
it->ru 意大利語翻譯為俄羅斯語
翻譯程序默認(rèn)是 en->es ,并提供了兩個(gè)命令: to 和 from 來實(shí)現(xiàn)語言的切換。比如,輸入 to es 將會(huì)把翻譯的目標(biāo)語言設(shè)置為西班牙語。輸入 quit 可以退出程序。
如果用戶輸入的字符串不是命令的話,翻譯程序會(huì)把輸入逐字地發(fā)送到翻譯的 web 服務(wù)。然后把返回的結(jié)果打印出來。
需要注意的幾點(diǎn)
如果你是系統(tǒng)或者運(yùn)維程序員,并且以前也沒接觸過 Swift 的話,下面是一些你在代碼里需要注意的事情。我想你會(huì)發(fā)現(xiàn) Swift 為兩種類型的工程師都提供了很多有用的特性,并且會(huì)成為 Linux 開發(fā)體系中一股很受歡迎的新力量。
let variable = value 常量賦值
元組(tuples)
switch-case 支持字符串
switch-case 使用時(shí)必須包含所有情況(邏輯完備性)
計(jì)算型 屬性
import Glibc 可以導(dǎo)入標(biāo)準(zhǔn)的 C 函數(shù)
guard 語句
可以使用 NSThread 和 NSNotificationCenter 這些蘋果的 Foundation 框架中的類。
在不同的線程或不同的對(duì)象里通過發(fā)送消息來觸發(fā)特定代碼的執(zhí)行
程序設(shè)計(jì)
我們的翻譯程序可以拆分成一個(gè)主程序、兩個(gè)類以及一個(gè) globals.swift 文件。如果你打算跟著做,那你應(yīng)該使用 Swift 的包管理器 ,然后調(diào)整你的目錄結(jié)構(gòu)為下面這樣:
- translator/Sources/main.swift
 - /Sources/CommandInterpreter.swift
 - /Sources/...
 - /Package.swift
 
main.swift 文件是 Swift 應(yīng)用程序的入口并且應(yīng)該是唯一一個(gè)包含可執(zhí)行代碼的文件(在這里,像「變量賦值」,或者「聲明一個(gè)類」不屬于「可執(zhí)行的代碼」)。
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)
 
上面的代碼表示我們的程序不接受命令行參數(shù)。具體的流程說明:
分別創(chuàng)建 CommandInterpreter 和 Translator 類的實(shí)例
為 InputNotification 通知添加觀察者(這里用到的常量 INPUT_NOTIFICATION 常量定義在 globals.swift )
添加當(dāng)收到通知的時(shí)候要執(zhí)行的代碼
調(diào)用 Interpreter 類實(shí)例的 start 方法
調(diào)用 select 來實(shí)現(xiàn)當(dāng)程序有其他線程在運(yùn)行的時(shí)候,鎖定主線程。(譯注:也就是防止主線程提前結(jié)束)
CommandInterpreter 類
CommandInterpreter 類主要負(fù)責(zé)從終端讀入輸入的字符串,并且分析輸入的類型并分別進(jìn)行處理??紤]到你可能剛接觸 Swift,我在代碼里對(duì)涉及到語言特性的地方進(jìn)行了注釋。
- // 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 類的實(shí)現(xiàn)邏輯非常直觀。當(dāng) start 函數(shù)被調(diào)用的時(shí)候,通過 NSThread 來創(chuàng)建一個(gè)線程,線程中再通過 block fgetc 的回調(diào)參數(shù) stdin 來獲取終端的輸入。當(dāng)遇到換行符 RETURN (用戶按了回車)后,輸入的字符串會(huì)被解析并映射成一個(gè) Command 對(duì)象。然后傳遞給 doCommand 函數(shù)進(jìn)行剩下的處理。
我們的 doCommand 函數(shù)就是一個(gè)簡單的 switch-case 語句。對(duì)于 .Quit 命令則就簡單調(diào)用 exit(0) 來終止程序。 .SetFrom 和 .SetTo 命令的功能是顯而易見的。當(dāng)遇到 .Translate 命令時(shí),F(xiàn)oundation 的消息系統(tǒng)就派上用場了。 doCommand 函數(shù)自己并不完成任何的翻譯功能,它只是簡單的 發(fā)送 一個(gè)應(yīng)用程序級(jí)別的消息,也就是 InputNotification 。任何監(jiān)聽這個(gè)消息的代碼都會(huì)被調(diào)用(比如我們之前的主線程):
- // 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!)
 - }
 - }
 
我在 這篇文章 中提到,在對(duì) NSNotification 的 userInfo 字典做類型轉(zhuǎn)換時(shí)會(huì)有一個(gè) SILGen 的閃退 crash,在這里我們用一個(gè)叫做 translationCommand 的全局變量來繞過這個(gè) crash。在這段代碼里:
為了代碼的簡潔,把 translationCommand 的內(nèi)容賦值給 tc
調(diào)用 Translator 對(duì)象的 translate 方法,并傳入相關(guān)的參數(shù)
實(shí)現(xiàn)翻譯完成后的回調(diào)
用一個(gè) Swift 漂亮的 guard 語句來檢測是否有錯(cuò)并返回
打印出翻譯的文本
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)!)
 - }
 - }
 
整合各個(gè)部分
要把上面介紹的組件結(jié)合到一起,我們還需要?jiǎng)?chuàng)建額外的兩個(gè)文件: 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)
 - ]
 - )
 
如果一切都配置正確的話,最后執(zhí)行 swift build ,一個(gè)極具特色的翻譯程序就完成了。
- 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
 
試試自己動(dòng)手
現(xiàn)在的翻譯程序還有很多可以優(yōu)化的地方。下面是一個(gè)你可以嘗試的列表:
- 接受命令行參數(shù)來設(shè)置默認(rèn)的源語言和目標(biāo)語言
 - 接受命令行參數(shù)來實(shí)現(xiàn)非交互模式
 - 添加 swap 命令來交換源語言和目標(biāo)語言
 - 添加 help 命令
 - 整合 from 命令和 to 命令。實(shí)現(xiàn)一行可以同時(shí)設(shè)置兩者, 比如 from en to es
 - 現(xiàn)在當(dāng)輸入 from 命令和 to 命令時(shí),沒有同時(shí)輸入對(duì)應(yīng)的語言時(shí)會(huì)崩潰,修復(fù)這個(gè)BUG
 - 實(shí)現(xiàn)對(duì)轉(zhuǎn)義符 \ 的處理,實(shí)現(xiàn)程序的“命令”也可以被翻譯(比如退出命令:quit)
 - 通過 localizedDescription 對(duì)錯(cuò)誤提示添加本地化的支持
 - 在 Translator 類中實(shí)現(xiàn)但有錯(cuò)誤發(fā)生時(shí),通過 throws 來處理異常
 
結(jié)束語
試試自己動(dòng)手
現(xiàn)在的翻譯程序還有很多可以優(yōu)化的地方。下面是一個(gè)你可以嘗試的列表:
- 接受命令行參數(shù)來設(shè)置默認(rèn)的源語言和目標(biāo)語言
 - 接受命令行參數(shù)來實(shí)現(xiàn)非交互模式
 - 添加 
swap命令來交換源語言和目標(biāo)語言 - 添加 
help命令 - 整合 
from命令和to命令。實(shí)現(xiàn)一行可以同時(shí)設(shè)置兩者, 比如from en to es - 現(xiàn)在當(dāng)輸入 
from命令和to命令時(shí),沒有同時(shí)輸入對(duì)應(yīng)的語言時(shí)會(huì)崩潰,修復(fù)這個(gè)BUG - 實(shí)現(xiàn)對(duì)轉(zhuǎn)義符 
\的處理,實(shí)現(xiàn)程序的“命令”也可以被翻譯(比如退出命令:quit) - 通過 
localizedDescription對(duì)錯(cuò)誤提示添加本地化的支持 - 在 
Translator類中實(shí)現(xiàn)但有錯(cuò)誤發(fā)生時(shí),通過throws來處理異常 
結(jié)束語
我從來不掩飾我是一個(gè)狂熱的 Swift 愛好者,我堅(jiān)信它很可能既能像 Perl、Python 和 Ruby 這樣語言一樣出色的完成運(yùn)維工作,也能像 C、C++ 和 Java 一樣出色的完成系統(tǒng)編程的任務(wù)。我知道現(xiàn)在和那些個(gè)單文件腳本語言相比,Swift 比較蛋疼的一點(diǎn)就是必須得編譯成二進(jìn)制文件。我真誠的希望這一點(diǎn)能夠改善,這樣我就能不再關(guān)注語言層面的東西而是去做一些新,酷酷的東西。
我真誠的希望這一點(diǎn)能夠改善,這樣我就能不再關(guān)注語言層面的東西而是去做一些新,酷酷的東西。
















 
 
 









 
 
 
 