Xcode创建终端命令
典型的Mac用户交互都是基于鼠标在屏幕上操作图形元素来交互的.在这种交互方式之前,是用命令行和电脑交流的.命令行基于文本信息,键入程序名来运行,可选地带上参数.
尽管图形界面很方便,但命令行程序依然在今天有很重要的作用.ImageMagick和ffmpeg在服务器中是很重要的命令行程序.实际上大多数网络服务都只运行命令行程序.
这篇文章会教你写一个叫Panagram的命令行程序.他会根据你传入的参数,来判断是回文还是变位词.它可以以预定义的参数开始,或者交互模式下,提示用户输入所需的值。
通常,命令行程序从shell启动,比macOS中的终端的bash shell.简单起见和方便学习,大多数时间我们都在Xcode启动命令行程序.最后才讲从终端启动命令行程序.
Getting Started
Swift相对于传统的C, Perl, Ruby 或者 Java语言创建命令行程序是比较奇特的选择.
但是选他肯定是有原因的:
1.Swift可以作为解释型的脚本语言,也可以作为编译语言.作为脚本语言可以省去编译,易于维护.作为编译语言可以提高运行效率,或者打包出售给社会.
2.不必切换语言.和多人说程序员每年都要学一门新语言.这是个好想法,但是如果你已经熟悉了Swift和它的标准库,你用Swift就是节约了时间.
这个教程会教你创建一个的编译项目.
在Product Name, 输入Panagram. 确保语言是Swift, 然后点击Next:
选择一个位置存储你的项目,然后点击Create.
在Project Navigator area你会看到由末模板创建的main.swift文件
很多类C语言都有一个main函数作为程序入口.也就意味着程序启动一启动就执行这个函数的第一行代码.相反, Swift没有main函数,但是它有一个main文件.
当你运行程序的时候,会执行第一行不是方法或者类声明的代码.所以保持main.swift文件整洁很重要.把类和结构放到他们自己的文件里.这样不仅合理,而且容易理解程序的执行路径.
The Output Stream
大多数命令行程序都会打印一些信息给用户看.比如一个视屏格式转换器就会打印当前的进度或者错误信息.
类unix比如macOS就定义两种不一样的输出流:
1.标准输出流(stdout),通常定向到显示器,显示信息给用户.
2.标准错误流(stdout),用来显示转台和错误信息.一般定向到显示器,也可以定向到一个文件.
注意:无论是从Xcode还是终端启动命令行程序,默认情况下stdout 和 stderr是一样的,而且输出的信息,都会写到控制台里.实际当中都会把stderr定向到文件中,便于之后查看.这样可以把用户不必知道的错误信息隐藏存储起来,之后可以再慢慢根据这些错误信息修复bug.
在Project navigator选中Panagram,然后按 Cmd + N创建新文件,选择Source/Swift File,按Next:
把文件存储为ConsoleIO.swift,之后会封装input和output方法到一个小小的类,命名为ConsoleIO.
添加下面的代码到ConsoleIO.swift的最后面:
class ConsoleIO {
}
解析来的任务就是让Panagram使用这两个输出流.
在ConsoleIO.swift添加下面这个枚举类型到import行和ConsoleIO类实现之间:
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)
}
}
这个方法有两个参数,第一个是要输出的信息,第二个是输出的目标地址,默认值是.standard.
.standard选项使用print,会写入到stdout. .error选项会使用c函数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()方法会打印使用信息到控制台.每次运行程序,可执行文件的路径都在 argument[0]里,而 argument[0]可以通过全局枚举的CommandLine访问到.CommandLine是围绕argc和argv参数的Swift标准库中的封装的代码
注意:实际当中,当用户使用错了参数,都会打印使用信息到控制台.
创建另一个文件Panagram.swift .添加以下代码:
class Panagram {
let consoleIO = ConsoleIO()
func staticMode() {
consoleIO.printUsage()
}
}
定义了一个有一个方法的Panagram类.这个类会处理程序的主要逻辑.当前他只是简单地打印了使用信息.
现在打开main文件,把print语句替换成:
let panagram = Panagram()
panagram.staticMode()
注意:如之前描述,这些代码会成为程序最开始执行的代码.
编译运行项目,会显示如下信息在控制台:
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
好了,到现在为止,你应该知道什么是命令行工具,从哪里开始执行,注意发送信息到stdout和stdout.如何有组织地安排代码到各个文件.
下一节会处理参数,完成Panagram的static mode
Command-Line Arguments
运行命令行程序的时候,打在名字之后的东西都会成为参数传给程序,参数可以用空格分开.通常都使用两种参数: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参数直接传到init(_:)方法里.Panagram有三种options:-p检测回文,-a检测变位字,-h显示使用信息.除此之外的都为作为一个错误.
接下来添加下面代码到Panagram类里:
func getOption(_ option: String) -> (option:OptionType, value: String) {
return (OptionType(value: option), option)
}
上面的方法接受一个option参数作为一个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()里面的内容:
//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.取得参数的数量,因为执行路径总是存在(CommandLine.arguments[0]),所以数量总是大于等于1
- 从arguments数组获取真正的参数.
3.解析参数,转换成OptionType类型.index(_:offsetBy:)忽略了第一个字符,因为我们总是这里总是破折号.
4.输出解析结果到控制台.
在main文件里,替换panagram.staticMode()这一行:
if CommandLine.argc < 2 {
//TODO: Handle interactive mode
} else {
panagram.staticMode()
}
如果少于两个参数,就会开启交互模式(之后会讲).否则就是非交互模式静态模式.
现在,为了弄明白怎么用Xcode传参数到命令行工具.在Toolbar点击Panagram的Scheme:
选择Edit Scheme:
Edit_Scheme.png
确保在左面板里选择的是Run,点击Arguments页,在Arguments Passed On Launch下点+号,添加-p作为参数,然后关闭。
然后运行程序,你会看到如下信息在控制台
Argument count: 2 Option: Palindrome value: p
Program ended with exit code: 0
现在,你已经添加了一个option系统,明白如何处理参数和通过Xcode传参数.接下来会介绍Panagram的主要功能.
Anagrams and Palindromes
在你写代码检测回文和变位词之前,你得知道什么是回文和变位词.
回文就是从前往后的或者从后往前读都是一样的,比如:
level
noon
A man, a plan, a canal - Panama!
可以看到,标点符号和大小写是忽略的,所以在程序里面我们也会忽略.
变位字就是用其他单词或者句子里的字符生成的单词或者句子,比如:
silent <-> listen
Bolivia <-> Lobivia(一种仙人掌)
新建一个StringExtension.swift文件,添加以下代码:
extension String {
}
讲一下检测变位字的基本流程:
1.忽略大小写和空格
2.检查是否包含同样的字符,所有字符出现的次数一样.
添加下面方法到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.反转字符比较,如果一样就是回文
添加如下方法检测回文:
func isPalindrome() -> Bool {
//1
let f = self.lowercased().replacingOccurrences(of: " ", with: "")
//2
let s = String(f.reversed())
//3
return f == s
}
逻辑是这样的
1.忽略大小写和空格
2.用反转字符
3.比较一致性,一样就是回文
把所有都拼起来,实现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.根据参数决定执行哪种操作
2.anagram情况下,必须有四个参数,可执行文件的路径,-a option和两个需要检测的词.如果不是四个参数就会报错.
3.如果参数正确,就是存储字符到本地变量,看他们是否是变位词,然后打印结果
4.palindrome情况下,必须有三个参数,第一个是执行路径,第二个是-p,第三个是是检查的词,如果不是三个,同样也会报错
5.检查是否是回文,打印结果
6.-h option传进来了,就会输出使用信息
7.传入未知option就会打印使用信息
编辑scheme里面的参数,添加level参数到scheme:
运行程序:
level is a palindrome
Program ended with exit code: 0
Handle Input Interactively
现在你有了一个Panagram的基本版本.我们可以添加额外的功能,让他可以通过输入参数带输入流来交互.
这一节,会添加代码,不传入一个参数让Panagram启动,进入交互模式,提示用户输入需要的内容.
首先你需要获得键盘的输入流, 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.读取数据
3.数据转换成字符
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打破死循环
3.提示用户选择模式
4.提升提示用户输入一个词或者两个词
5.输出结果
6.如果输入了未知option,提示错误,重新开始循环
现在你还没办法打断这个while循环.在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()
检测交互模式,你要把参数都清空掉:
运行:
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
创建一个会打开终端并且运行Panagram的Scheme:
命名为Panagram on Terminal:
选中Panagram on Terminal为激活状态,点击Edit Scheme.选择info,在Executable找到到Terminal.app,并选择.选择就会使用终端运行了,取消勾选Debug executable.
如下图:
接下来选择Arguments面板,添加一个新参数${BUILT_PRODUCTS_DIR}/${FULL_PRODUCT_NAME}:
最后关闭.
确认选择的是Panagram on Terminal scheme,运行程序.Xcode会打开终端:
Launch your app directly from Terminal
打开Applications/Utilities文件夹.
Project Navigator 里选择Products,拷贝右边的路径,不包括"Panagram":
打开finder,选择前往文件夹(快捷键 shift+command+G ),粘贴拷贝的路径并前往:
把Panagram可执行文件拖到终端的窗口里,按回车.Panagram就会进入交互模式,因为没有参数传入:
注意:如果想通过这种方式进入静态模式,就需要在按回车之前输入参数比如:-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用来设置正常情况下的文本颜色
2.\u{001B}[0;31m把文本颜色变成红色
运行,键入-f,就会显示红色信息:
ps:1.ncurses用来写GUI风格程序的C库
2.Scripting in Swift is pretty awesome Swift写脚本