典型的Mac用戶交互都是基于鼠標在屏幕上操作圖形元素來交互的.在這種交互方式之前,是用命令行和電腦交流的.命令行基于文本信息,鍵入程序名來運行,可選地帶上參數(shù).
盡管圖形界面很方便,但命令行程序依然在今天有很重要的作用.ImageMagick和ffmpeg在服務(wù)器中是很重要的命令行程序.實際上大多數(shù)網(wǎng)絡(luò)服務(wù)都只運行命令行程序.
這篇文章會教你寫一個叫Panagram的命令行程序.他會根據(jù)你傳入的參數(shù),來判斷是回文還是變位詞.它可以以預(yù)定義的參數(shù)開始鼻由,或者交互模式下呆万,提示用戶輸入所需的值。
通常,命令行程序從shell啟動,比macOS中的終端的bash shell.簡單起見和方便學習,大多數(shù)時間我們都在Xcode啟動命令行程序.最后才講從終端啟動命令行程序.
Getting Started
Swift相對于傳統(tǒng)的C, Perl, Ruby 或者 Java語言創(chuàng)建命令行程序是比較奇特的選擇.
但是選他肯定是有原因的:
1.Swift可以作為解釋型的腳本語言,也可以作為編譯語言.作為腳本語言可以省去編譯,易于維護.作為編譯語言可以提高運行效率,或者打包出售給社會.
2.不必切換語言.和多人說程序員每年都要學一門新語言.這是個好想法,但是如果你已經(jīng)熟悉了Swift和它的標準庫,你用Swift就是節(jié)約了時間.
這個教程會教你創(chuàng)建一個的編譯項目.
在Product Name, 輸入Panagram. 確保語言是Swift, 然后點擊Next:
選擇一個位置存儲你的項目,然后點擊Create.
在Project Navigator area你會看到由末模板創(chuàng)建的main.swift文件
很多類C語言都有一個main函數(shù)作為程序入口.也就意味著程序啟動一啟動就執(zhí)行這個函數(shù)的第一行代碼.相反, Swift沒有main函數(shù),但是它有一個main文件.
當你運行程序的時候,會執(zhí)行第一行不是方法或者類聲明的代碼.所以保持main.swift文件整潔很重要.把類和結(jié)構(gòu)放到他們自己的文件里.這樣不僅合理,而且容易理解程序的執(zhí)行路徑.
The Output Stream
大多數(shù)命令行程序都會打印一些信息給用戶看.比如一個視屏格式轉(zhuǎn)換器就會打印當前的進度或者錯誤信息.
類unix比如macOS就定義兩種不一樣的輸出流:
1.標準輸出流(stdout),通常定向到顯示器,顯示信息給用戶.
2.標準錯誤流(stdout),用來顯示轉(zhuǎn)臺和錯誤信息.一般定向到顯示器,也可以定向到一個文件.
注意:無論是從Xcode還是終端啟動命令行程序,默認情況下stdout 和 stderr是一樣的,而且輸出的信息,都會寫到控制臺里.實際當中都會把stderr定向到文件中,便于之后查看.這樣可以把用戶不必知道的錯誤信息隱藏存儲起來,之后可以再慢慢根據(jù)這些錯誤信息修復(fù)bug.
在Project navigator選中Panagram,然后按 Cmd + N創(chuàng)建新文件,選擇Source/Swift File,按Next:把文件存儲為ConsoleIO.swift,之后會封裝input和output方法到一個小小的類,命名為ConsoleIO.
添加下面的代碼到ConsoleIO.swift的最后面:
class ConsoleIO {
}
解析來的任務(wù)就是讓Panagram使用這兩個輸出流.
在ConsoleIO.swift添加下面這個枚舉類型到import行和ConsoleIO類實現(xiàn)之間:
enum OutputType {
case error
case standard
}
這樣就定義了輸出信息時使用的輸出流.
接下來把下面這個方法到ConsoleIO 到類里(在這個類的花括號里):
func writeMessage(_ message: String, to: OutputType = .standard) {
switch to {
case .standard:
print("\(message)")
case .error:
fputs("Error: \(message)\n", stderr)
}
}
這個方法有兩個參數(shù),第一個是要輸出的信息,第二個是輸出的目標地址,默認值是.standard.
.standard選項使用print,會寫入到stdout. .error選項會使用c函數(shù)fputs寫入信息到全局并且指向標準錯誤流的stderr
再增加以下代碼到ConsoleIO類里:
func printUsage() {
let executableName = (CommandLine.arguments[0] as NSString).lastPathComponent
writeMessage("usage:")
writeMessage("\(executableName) -a string1 string2")
writeMessage("or")
writeMessage("\(executableName) -p string")
writeMessage("or")
writeMessage("\(executableName) -h to show usage information")
writeMessage("Type \(executableName) without an option to enter interactive mode.")
}
printUsage()方法會打印使用信息到控制臺.每次運行程序,可執(zhí)行文件的路徑都在 argument[0]里,而 argument[0]可以通過全局枚舉的CommandLine訪問到.CommandLine是圍繞argc和argv參數(shù)的Swift標準庫中的封裝的代碼
注意:實際當中,當用戶使用錯了參數(shù),都會打印使用信息到控制臺.
創(chuàng)建另一個文件Panagram.swift .添加以下代碼:
class Panagram {
let consoleIO = ConsoleIO()
func staticMode() {
consoleIO.printUsage()
}
}
定義了一個有一個方法的Panagram類.這個類會處理程序的主要邏輯.當前他只是簡單地打印了使用信息.
現(xiàn)在打開main文件,把print語句替換成:
let panagram = Panagram()
panagram.staticMode()
注意:如之前描述,這些代碼會成為程序最開始執(zhí)行的代碼.
編譯運行項目,會顯示如下信息在控制臺:
usage:
Panagram -a string1 string2
or
Panagram -p string
or
Panagram -h to show usage information
Type Panagram without an option to enter interactive mode.
Program ended with exit code: 0
好了,到現(xiàn)在為止,你應(yīng)該知道什么是命令行工具,從哪里開始執(zhí)行,注意發(fā)送信息到stdout和stdout.如何有組織地安排代碼到各個文件.
下一節(jié)會處理參數(shù),完成Panagram的static mode
Command-Line Arguments
運行命令行程序的時候,打在名字之后的東西都會成為參數(shù)傳給程序,參數(shù)可以用空格分開.通常都使用兩種參數(shù):options 和 strings.
Options的起始是一個破折號,之后跟著一個字符,或者兩個破折號跟著一個單詞.比如很多程序都有-h 或者 --help選項,前者是后者的簡化.為了簡單化,我們使用前者.
打開Panagram.swift,添加下面代碼到文件的最上面,在Panagram類的范圍之外:
enum OptionType: String {
case palindrome = "p"
case anagram = "a"
case help = "h"
case unknown
init(value: String) {
switch value {
case "a": self = .anagram
case "p": self = .palindrome
case "h": self = .help
default: self = .unknown
}
}
}
上面代碼里用字符定義了一個枚舉類型,可以把option參數(shù)直接傳到init(_:)方法里.Panagram有三種options:-p檢測回文,-a檢測變位字,-h顯示使用信息.除此之外的都為作為一個錯誤.
接下來添加下面代碼到Panagram類里:
func getOption(_ option: String) -> (option:OptionType, value: String) {
return (OptionType(value: option), option)
}
上面的方法接受一個option參數(shù)作為一個String,然后返回String和OptionType的一個元組.
注意:如果你不熟悉元組,看看我們的視頻[PART 5: Tuples](https://videos.raywenderlich.com/courses/51-beginning-swift-3/lessons/5?_ga=2.76197205.1321793565.1518402701-549624587.1518402701)
在Panagram里,用下面代碼替換staticMode()里面的內(nèi)容:
//1
let argCount = CommandLine.argc
//2
let argument = CommandLine.arguments[1]
//3
let (option, value) = getOption(argument.substring(from: argument.index(argument.startIndex, offsetBy: 1)))
//4
consoleIO.writeMessage("Argument count: \(argCount) Option: \(option) value: \(value)")
解釋一下代碼:
1.取得參數(shù)的數(shù)量,因為執(zhí)行路徑總是存在(CommandLine.arguments[0]),所以數(shù)量總是大于等于1
- 從arguments數(shù)組獲取真正的參數(shù).
3.解析參數(shù),轉(zhuǎn)換成OptionType類型.index(_:offsetBy:)忽略了第一個字符,因為我們總是這里總是破折號.
4.輸出解析結(jié)果到控制臺.
在main文件里,替換panagram.staticMode()這一行:
if CommandLine.argc < 2 {
//TODO: Handle interactive mode
} else {
panagram.staticMode()
}
如果少于兩個參數(shù),就會開啟交互模式(之后會講).否則就是非交互模式靜態(tài)模式.
現(xiàn)在,為了弄明白怎么用Xcode傳參數(shù)到命令行工具.在Toolbar點擊Panagram的Scheme:
選擇Edit Scheme:
確保在左面板里選擇的是Run,點擊Arguments頁煎谍,在Arguments Passed On Launch下點+號,添加-p作為參數(shù)净赴,然后關(guān)閉哩陕。
然后運行程序,你會看到如下信息在控制臺
Argument count: 2 Option: Palindrome value: p
Program ended with exit code: 0
現(xiàn)在,你已經(jīng)添加了一個option系統(tǒng),明白如何處理參數(shù)和通過Xcode傳參數(shù).接下來會介紹Panagram的主要功能.
Anagrams and Palindromes
在你寫代碼檢測回文和變位詞之前,你得知道什么是回文和變位詞.
回文就是從前往后的或者從后往前讀都是一樣的,比如:
level
noon
A man, a plan, a canal - Panama!
可以看到,標點符號和大小寫是忽略的,所以在程序里面我們也會忽略.
變位字就是用其他單詞或者句子里的字符生成的單詞或者句子,比如:
silent <-> listen
Bolivia <-> Lobivia(一種仙人掌)
新建一個StringExtension.swift文件,添加以下代碼:
extension String {
}
講一下檢測變位字的基本流程:
1.忽略大小寫和空格
2.檢查是否包含同樣的字符,所有字符出現(xiàn)的次數(shù)一樣.
添加下面方法到StringExtension.swift:
func isAnagramOf(_ s: String) -> Bool {
//1
let lowerSelf = self.lowercased().replacingOccurrences(of: " ", with: "")
let lowerOther = s.lowercased().replacingOccurrences(of: " ", with: "")
//2
return lowerSelf.sorted() == lowerOther.sorted()
}
闡述一下上面的邏輯:
1.移除大小寫和空格
2.比較排過序的字符
檢測回文就簡單了
1.忽略大小寫和空格
2.反轉(zhuǎn)字符比較,如果一樣就是回文
添加如下方法檢測回文:
func isPalindrome() -> Bool {
//1
let f = self.lowercased().replacingOccurrences(of: " ", with: "")
//2
let s = String(f.reversed())
//3
return f == s
}
邏輯是這樣的
1.忽略大小寫和空格
2.用反轉(zhuǎn)字符
3.比較一致性,一樣就是回文
把所有都拼起來,實現(xiàn)Panagram的功能
打開Panagram.swift,替換staticMode()里面的writeMessage(_:to:):
//1
switch option {
case .anagram:
//2
if argCount != 4 {
if argCount > 4 {
consoleIO.writeMessage("Too many arguments for option \(option.rawValue)", to: .error)
} else {
consoleIO.writeMessage("Too few arguments for option \(option.rawValue)", to: .error)
}
consoleIO.printUsage()
} else {
//3
let first = CommandLine.arguments[2]
let second = CommandLine.arguments[3]
if first.isAnagramOf(second) {
consoleIO.writeMessage("\(second) is an anagram of \(first)")
} else {
consoleIO.writeMessage("\(second) is not an anagram of \(first)")
}
}
case .palindrome:
//4
if argCount != 3 {
if argCount > 3 {
consoleIO.writeMessage("Too many arguments for option \(option.rawValue)", to: .error)
} else {
consoleIO.writeMessage("Too few arguments for option \(option.rawValue)", to: .error)
}
consoleIO.printUsage()
} else {
//5
let s = CommandLine.arguments[2]
let isPalindrome = s.isPalindrome()
consoleIO.writeMessage("\(s) is \(isPalindrome ? "" : "not ")a palindrome")
}
//6
case .help:
consoleIO.printUsage()
case .unknown:
//7
consoleIO.writeMessage("Unknown option \(value)")
consoleIO.printUsage()
}
1.根據(jù)參數(shù)決定執(zhí)行哪種操作
2.anagram情況下,必須有四個參數(shù),可執(zhí)行文件的路徑,-a option和兩個需要檢測的詞.如果不是四個參數(shù)就會報錯.
3.如果參數(shù)正確,就是存儲字符到本地變量,看他們是否是變位詞,然后打印結(jié)果
4.palindrome情況下,必須有三個參數(shù),第一個是執(zhí)行路徑,第二個是-p,第三個是是檢查的詞,如果不是三個,同樣也會報錯
5.檢查是否是回文,打印結(jié)果
6.-h option傳進來了,就會輸出使用信息
7.傳入未知option就會打印使用信息
編輯scheme里面的參數(shù),添加level參數(shù)到scheme:
運行程序:
level is a palindrome
Program ended with exit code: 0
Handle Input Interactively
現(xiàn)在你有了一個Panagram的基本版本.我們可以添加額外的功能,讓他可以通過輸入?yún)?shù)帶輸入流來交互.
這一節(jié),會添加代碼,不傳入一個參數(shù)讓Panagram啟動,進入交互模式,提示用戶輸入需要的內(nèi)容.
首先你需要獲得鍵盤的輸入流, stdin就指向了鍵盤.
打開ConsoleIO.swift,增加下面方法到這個類:
func getInput() -> String {
// 1
let keyboard = FileHandle.standardInput
// 2
let inputData = keyboard.availableData
// 3
let strData = String(data: inputData, encoding: String.Encoding.utf8)!
// 4
return strData.trimmingCharacters(in: CharacterSet.newlines)
}
代碼邏輯:
1.獲取鍵盤輸入
2.讀取數(shù)據(jù)
3.數(shù)據(jù)轉(zhuǎn)換成字符
4.移除換行返回文字
然后,打開Panagram.swift,添加下面方法:
func interactiveMode() {
//1
consoleIO.writeMessage("Welcome to Panagram. This program checks if an input string is an anagram or palindrome.")
//2
var shouldQuit = false
while !shouldQuit {
//3
consoleIO.writeMessage("Type 'a' to check for anagrams or 'p' for palindromes type 'q' to quit.")
let (option, value) = getOption(consoleIO.getInput())
switch option {
case .anagram:
//4
consoleIO.writeMessage("Type the first string:")
let first = consoleIO.getInput()
consoleIO.writeMessage("Type the second string:")
let second = consoleIO.getInput()
//5
if first.isAnagramOf(second) {
consoleIO.writeMessage("\(second) is an anagram of \(first)")
} else {
consoleIO.writeMessage("\(second) is not an anagram of \(first)")
}
case .palindrome:
consoleIO.writeMessage("Type a word or sentence:")
let s = consoleIO.getInput()
let isPalindrome = s.isPalindrome()
consoleIO.writeMessage("\(s) is \(isPalindrome ? "" : "not ")a palindrome")
default:
//6
consoleIO.writeMessage("Unknown option \(value)", to: .error)
}
}
}
解釋代碼:
1.打印歡迎信息
2.shouldQuit打破死循環(huán)
3.提示用戶選擇模式
4.提升提示用戶輸入一個詞或者兩個詞
5.輸出結(jié)果
6.如果輸入了未知option,提示錯誤,重新開始循環(huán)
現(xiàn)在你還沒辦法打斷這個while循環(huán).在Panagram.swift添加下面這一行到OptionType,枚舉里面:
case quit = "q"
然后添加下面這行到枚舉的init(_:)里:
case "q": self = .quit
同一個文件添加一個.quit case到interactiveMode()的switch語句里:
case .quit:
shouldQuit = true
然后修改staticMode()里.unknown case的定義成下面的樣子:
case .unknown, .quit:
打開main.swift 替換注釋成:
panagram.interactiveMode()
檢測交互模式,你要把參數(shù)都清空掉:
運行:
Welcome to Panagram. This program checks if an input string is an anagram or palindrome.
Type 'a' to check for anagrams or 'p' for palindromes type 'q' to quit.
試一試不同的模式:在每個輸入之后回車:
a
Type the first string:
silent
Type the second string:
listen
listen is an anagram of silent
Type 'a' to check for anagrams or 'p' for palindromes type 'q' to quit.
p
Type a word or sentence:
level
level is a palindrome
Type 'a' to check for anagrams or 'p' for palindromes type 'q' to quit.
f
Error: Unknown option f
Type 'a' to check for anagrams or 'p' for palindromes type 'q' to quit.
q
Program ended with exit code: 0
Launching Outside Xcode
通常命令行程序都是在通過終端里面運行的.
有很多種通過終端運行的方式.通過編譯后的二進制包直接通過終端運行,或者讓Xcode幫你.
Launch your app in Terminal from Xcode
創(chuàng)建一個會打開終端并且運行Panagram的Scheme:
命名為Panagram on Terminal:
選中Panagram on Terminal為激活狀態(tài),點擊Edit Scheme.選擇info,在Executable找到到Terminal.app,并選擇.選擇就會使用終端運行了,取消勾選Debug executable.
如下圖:
接下來選擇Arguments面板,添加一個新參數(shù)${BUILT_PRODUCTS_DIR}/${FULL_PRODUCT_NAME}:
最后關(guān)閉.
確認選擇的是Panagram on Terminal scheme,運行程序.Xcode會打開終端:
Launch your app directly from Terminal
打開Applications/Utilities文件夾.
Project Navigator 里選擇Products,拷貝右邊的路徑,不包括"Panagram":
打開finder,選擇前往文件夾(快捷鍵 shift+command+G ),粘貼拷貝的路徑并前往:
把Panagram可執(zhí)行文件拖到終端的窗口里,按回車.Panagram就會進入交互模式,因為沒有參數(shù)傳入:
注意:如果想通過這種方式進入靜態(tài)模式,就需要在按回車之前輸入?yún)?shù)比如:-p level 或則 -a silent listen.
Displaying Errors
最后會添加用來顯示紅色錯誤信息的代碼.
打開ConsoleIO.swift,到writeMessage(_:to:),替換兩個case成:
case .standard:
// 1
print("\u{001B}[;m\(message)")
case .error:
// 2
fputs("\u{001B}[0;31m\(message)\n", stderr)
1.\u{001B}[;m用來設(shè)置正常情況下的文本顏色
2.\u{001B}[0;31m把文本顏色變成紅色
運行,鍵入-f,就會顯示紅色信息:
ps:1.ncurses用來寫GUI風格程序的C庫
2.Scripting in Swift is pretty awesome Swift寫腳本