美文网首页iOS Development
Xcode创建终端命令

Xcode创建终端命令

作者: OscarWang_ux | 来源:发表于2018-02-12 16:00 被阅读1383次

    典型的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写脚本

    相关文章

      网友评论

        本文标题:Xcode创建终端命令

        本文链接:https://www.haomeiwen.com/subject/egsrtftx.html