iOS Development

Xcode创建终端命令

2018-02-12  本文已影响1383人  OscarWang_ux

典型的Mac用户交互都是基于鼠标在屏幕上操作图形元素来交互的.在这种交互方式之前,是用命令行和电脑交流的.命令行基于文本信息,键入程序名来运行,可选地带上参数.
尽管图形界面很方便,但命令行程序依然在今天有很重要的作用.ImageMagickffmpeg在服务器中是很重要的命令行程序.实际上大多数网络服务都只运行命令行程序.
这篇文章会教你写一个叫Panagram的命令行程序.他会根据你传入的参数,来判断是回文还是变位词.它可以以预定义的参数开始,或者交互模式下,提示用户输入所需的值。
通常,命令行程序从shell启动,比macOS中的终端的bash shell.简单起见和方便学习,大多数时间我们都在Xcode启动命令行程序.最后才讲从终端启动命令行程序.

Getting Started

Swift相对于传统的C, Perl, Ruby 或者 Java语言创建命令行程序是比较奇特的选择.
但是选他肯定是有原因的:
1.Swift可以作为解释型的脚本语言,也可以作为编译语言.作为脚本语言可以省去编译,易于维护.作为编译语言可以提高运行效率,或者打包出售给社会.
2.不必切换语言.和多人说程序员每年都要学一门新语言.这是个好想法,但是如果你已经熟悉了Swift和它的标准库,你用Swift就是节约了时间.

这个教程会教你创建一个的编译项目.

打开 Xcode 来到 File/New/Project. 找到macOS组, 选择Application/Command Line Tool,点击Next:
在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

  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写脚本

上一篇下一篇

猜你喜欢

热点阅读