[macOS翻译]macOS上的命令行程序教程

803 阅读18分钟

原文地址:www.raywenderlich.com/511-command…

原文作者:www.raywenderlich.com/u/ericwasta…

发布时间:2017年7月21日 - 文章(30分钟) - 初学者

image.png

通过这个macOS上的命令行程序教程,发现制作自己的基于终端的应用程序是多么容易。针对Xcode 9和Swift 4进行了更新!

7/21/17更新:这个macOS上的命令行程序教程已经针对Xcode 9和Swift 4进行了更新。

典型的Mac用户使用图形用户界面(GUI)与他们的计算机进行交互。顾名思义,GUI是基于用户通过输入设备(如鼠标)与计算机进行视觉互动,选择或操作屏幕上的元素,如菜单、按钮等。

不久以前,在图形用户界面出现之前,命令行界面(CLI)是与计算机进行交互的主要方法。CLI是基于文本的界面,用户在其中键入要执行的程序名称,后面还可以加上参数。

尽管图形用户界面很盛行,但命令行程序在今天的计算世界中仍有重要作用。像ImageMagickffmpeg这样的命令行程序在服务器世界中很重要。事实上,构成互联网的大多数服务器只运行命令行程序。

甚至Xcode也使用命令行程序! 当Xcode构建你的项目时,它调用xcodebuild,它进行实际的构建。如果构建过程被烘烤到Xcode产品中,持续集成解决方案将很难实现,甚至是不可能的

在这个macOS的命令行程序教程中,你将编写一个名为Panagram的命令行工具。根据传入的选项,它将检测一个给定的输入是否是一个复数或变形词。它可以用预定义的参数启动,也可以在互动模式下运行,提示用户输入所需的值。

通常情况下,命令行程序是从一个嵌入到实用程序(如macOS中的bash shell)中的shell(如macOS中的终端)启动的。为了简单和便于学习,在本教程中,大部分时间你将使用Xcode来启动Panagram。在本教程的最后,你将学习如何从终端启动Panagram。

开始学习

对于创建一个命令行程序来说,Swift似乎是一个奇怪的选择,因为像C、Perl、Ruby或Java等语言是比较传统的选择。但是有一些很好的理由让你选择Swift来满足你的命令行需求。

  • Swift可以作为一种解释的脚本语言,也可以作为一种编译的语言。这给你带来了脚本语言的优势,如零编译时间和易于维护,同时还可以选择编译你的应用程序以提高执行时间,或将其捆绑起来向公众出售。
  • 你不需要转换语言。许多人说,一个程序员应该每年学习一门新的语言。这不是一个坏主意,但如果你已经习惯了Swift及其标准库,你可以通过坚持使用Swift来减少时间投入。

在本教程中,你将创建一个经典的编译项目。

打开Xcode,进入File/New/Project。找到macOS组,选择Application/Command Line Tool并点击Next。

image.png

对于产品名称,输入Panagram。确保语言被设置为Swift,然后点击下一步。

image.png

在你的磁盘上选择一个位置来保存你的项目,然后点击创建。

在项目导航区,你现在会看到由Xcode命令行工具模板创建的main.swift文件。

image.png

许多类C语言都有一个作为入口点的主函数--即程序执行时操作系统将调用的代码。这意味着程序的执行从这个函数的第一行开始。Swift没有一个主函数,相反,它有一个主文件。

当你运行你的项目时,main文件里面第一行不是方法或类的声明,就是第一个被执行的。尽量保持main.swift文件的干净,把所有的类和结构放在自己的文件中是个好主意。这可以使事情变得简单,并帮助你了解主要的执行路径。

输出流

在大多数命令行程序中,你想为用户打印一些信息。例如,一个将视频文件转换为不同格式的程序可以打印当前的进度,或者在出错时打印一些错误信息。

基于Unix的系统,如macOS,定义了两个不同的输出流。

  • 标准输出流(或stdout)通常连接到显示器上,应该用来向用户显示信息。
  • 标准错误流(或stderr)通常用于显示状态和错误信息。它通常连接在显示器上,但也可以重定向到一个文件。

注意: 当从Xcode或终端启动一个命令行程序时,默认情况下,stdout和stderr是一样的,两者的信息都写到控制台。通常的做法是将stderr重定向到一个文件,这样可以在以后查看从屏幕上滚动下来的错误信息。同时,这也可以通过隐藏用户不需要看到的信息,使调试一个已运行的应用程序变得更加容易,但仍然保留错误信息供以后检查。

在项目导航器中选择Panagram组,按Cmd+N创建一个新文件。在macOS下,选择Source/Swift File,然后按Next。

image.png

将该文件保存为ConsoleIO.swift。你将把所有的输入和输出元素包裹在一个名为ConsoleIO的小而方便的类中。

在ConsoleIO.swift的末尾添加以下代码。

class ConsoleIO {
}

你的下一个任务是改变Panagram以使用两个输出流。

在ConsoleIO.swift中,在文件的顶部,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()方法,将使用信息打印到控制台。每次你运行程序时,可执行文件的路径被隐含地作为参数[0]传递,并通过全局CommandLine枚举进行访问。CommandLine是Swift标准库中的一个小包装,围绕着你可能从类C语言中了解的argc和argv参数。

注意:当用户试图用不正确的参数启动一个命令行程序时,通常的做法是向控制台打印一个使用声明。 再创建一个名为Panagram.swift的新Swift文件(按照之前的步骤),并在其中添加以下代码。

class Panagram {

  let consoleIO = ConsoleIO()

  func staticMode() {
    consoleIO.printUsage()
  }

}

这定义了一个Panagram类,它有一个方法。该类将处理程序逻辑,staticMode()代表非交互式模式--即当你通过命令行参数提供所有数据时。现在,它只是打印出使用信息。

现在,打开main.swift,用以下代码替换打印语句。

let panagram = Panagram()
panagram.staticMode()

注意:如上所述,对于main.swift来说,这些是程序启动时将被执行的第一行代码。

构建并运行你的项目,你会在Xcode的控制台看到以下输出。

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和stderr发送信息,以及如何将你的代码分成逻辑单元以保持main.swift的条理性。

在下一节,你将处理命令行参数并完成Panagram的静态模式。

命令行参数

当你启动一个命令行程序时,你在名字后面输入的所有内容都会作为一个参数传递给程序。参数可以用空白字符分开。通常情况下,你会遇到两种参数:选项和字符串。

选项以一个破折号和一个字符开始,或以两个破折号和一个单词开始。例如,许多程序都有选项-h或-help,第一个选项只是第二个选项的快捷方式。为了保持简单,Panagram只支持选项的简短版本。

打开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
    }
  }
}

这定义了一个以String为基本类型的枚举,所以你可以直接将选项参数传递给init(_:)。Panagram有三个选项。-p用于检测回文,-a用于缩略语,-h用于显示使用信息。其他的都将被作为错误处理。

接下来,在Panagram类中添加以下方法。

func getOption(_ option: String) -> (option:OptionType, value: String) {
  return (OptionType(value: option), option)
}

上面的方法接受一个字符串的选项参数,并返回一个OptionType和String的元组。

注意:如果你还不熟悉Swift中的元组,请查看我们的视频系列Beginning Swift 3,特别是第五部分:元组

在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。
  2. 接下来,从参数数组中取出第一个 "真正的 "参数(选项参数)。
  3. 然后你解析该参数并将其转换为一个OptionType。index(_:offsetBy:)方法只是跳过参数字符串中的第一个字符,在本例中是选项前的连字符(-)。
  4. 最后,你把解析结果记录到Console中。

在main.swift中,将panagram.staticMode()一行替换为以下内容。

if CommandLine.argc < 2 {
  //TODO: Handle interactive mode
} else {
  panagram.staticMode()
}

如果你的程序被调用的参数少于2个,那么你就要启动交互式模式--这部分你以后再做。否则,你就使用非交互式的静态模式。

你现在需要弄清楚如何在Xcode中向你的命令行工具传递参数。要做到这一点,在工具栏中点击名为Panagram的Scheme。

image.png

在出现的菜单中选择Edit Scheme..:

image.png

确保在左窗格中选择了Run,点击Arguments标签,然后点击Arguments Passed On Launch下的+号。添加-p作为参数,然后点击关闭。

image.png

现在构建并运行,你会在控制台中看到以下输出。

Argument count: 2 Option: Palindrome value: p
Program ended with exit code: 0

到目前为止,你已经为你的工具添加了一个基本的选项系统,学会了如何处理命令行参数和如何从Xcode中传递参数。

接下来,您将建立Panagram的主要功能。

缩略语和平行四边形

在你写任何代码来检测宫格或变形词之前,你应该清楚它们是什么!

回文是指前后读起来相同的词或句子。这里有一些例子。

  • level
  • noon
  • A man, a plan, a canal - Panama!

正如你所看到的,大写字母和标点符号经常被忽略。为了保持简单,Panagram会忽略大写字母和空白,但不会处理标点符号。

缩略语是指使用其他单词或句子的字符建立的单词或句子。一些例子是。

  • silent listen
  • Bolivia Lobivia (it's a cactus from Bolivia)

你将把检测逻辑封装在String的一个小扩展中。

创建一个名为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. 在变位的情况下,必须有四个命令行参数传入。第一个是可执行路径,第二个是-a选项,最后是要检查的两个字符串。如果你没有四个参数,那么就打印一个错误信息。
  3. 如果参数数不错,就把这两个字符串储存在本地变量中,检查它们是否是彼此的重合,并打印结果。
  4. 如果是回文,你必须有三个参数。第一个是可执行路径,第二个是-p选项,最后是要检查的字符串。如果你没有三个参数,那么就打印一个错误信息。
  5. 检查字符串,看它是否是一个回文,并打印结果。
  6. 如果传入了-h选项,那么打印使用信息。
  7. 如果传入了一个未知的选项,则打印使用信息。

现在,修改方案里面的参数。例如,要使用-p选项,你必须传递两个参数(除了第一个参数,即可执行文件的路径,它总是以隐式方式传递)。

从 "设置活动方案 "工具栏项目中选择 "编辑方案...",并添加第二个参数,其值为 "水平",如下图所示。

image.png

建立并运行,你会在控制台看到以下输出。

level is a palindrome
Program ended with exit code: 0

交互式地处理输入

现在你已经有了一个基本版本的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. 首先,抓一个stdin的句柄。
  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. 如果用户输入了一个未知的选项,则打印一个错误并重新开始循环。

此刻,你没有办法中断while循环。在Panagram.swift中为OptionType枚举添加以下一行。

case quit = "q"

接下来,在枚举的init(_:)中添加以下一行。

case "q": self = .quit

在同一文件中,在interactiveMode()中的switch语句中添加一个.quit case。

case .quit:
  shouldQuit = true

然后,修改staticMode()中的.unknown案例定义如下。

case .unknown, .quit:

打开main.swift,将注释//TODO:处理互动模式改为以下内容。

panagram.interactiveMode()

为了测试交互式模式,你必须在Scheme中没有定义任何参数。

因此,删除你之前定义的两个参数。从工具栏菜单中选择编辑方案...。选择每个参数,然后点击启动时通过的参数下的-号。一旦所有参数被删除,点击关闭。

image.png

建立并运行,你会在控制台看到以下输出。

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.

试试不同的选项。键入一个选项字母(不要用连字符作前缀),然后是回车。你会被提示输入参数。输入每个值,然后是Return。在控制台中,你应该看到与此类似的东西。

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

在Xcode之外启动

通常情况下,一个命令行程序是从一个像终端这样的外壳工具中启动的(而不是从像Xcode这样的IDE中启动)。下面的部分将指导您在终端中启动您的应用程序。

有不同的方法可以通过终端来启动你的程序。你可以使用Finder找到编译好的二进制文件,然后直接通过终端启动它。或者,你可以偷懒,让Xcode为你做这个。首先,你将学习偷懒的方法。

在Xcode的终端中启动你的应用程序

创建一个新的方案,打开终端,在终端窗口中启动Panagram。在工具栏上点击名为Panagram的方案,选择新方案。

image.png

将新方案命名为Panagram on Terminal。

image.png

确保终端上的Panagram计划被选为活动计划。点击该计划,在弹出窗口中选择编辑计划...。

确保信息选项卡被选中,然后点击可执行文件下拉菜单,选择其他。现在,在你的应用程序/实用程序文件夹中找到Terminal.app,然后点击选择。现在Terminal是你的可执行文件,取消选择Debug executable。

你在Terminal方案的信息标签上的Panagram应该看起来像这样。

image.png

注意:这样做的缺点是你不能在Xcode中调试你的应用程序,因为现在Xcode在运行时启动的可执行文件是Terminal而不是Panagram。

接下来,选择Arguments标签,然后添加一个新的参数。

${built_products_dir}/${full_product_name}

image.png

最后,点击关闭。

现在,确保你选择了终端上的方案Panagram,然后构建并运行你的项目。Xcode将打开Terminal并传递给你的程序的路径。然后终端会像你所期望的那样启动你的程序。

image.png

直接从终端启动您的应用程序

从您的应用程序/实用工具文件夹中打开终端。

在项目导航器中选择产品组下的你的产品。从Xcode的实用程序区复制你的调试文件夹的全路径,如下图所示(不要包括 "Panagram")。

image.png

打开Finder窗口,选择Go/Go to Folder...菜单项,将您在上一步复制的完整路径粘贴到对话框的文本字段。

image.png

点击 "转到",Finder会导航到包含Panagram可执行文件的文件夹。

image.png

将Panagram可执行文件从Finder拖到终端窗口,并将其放在那里。切换到终端窗口并点击键盘上的返回键。由于没有指定参数,终端在交互式模式下启动了Panagram。

image.png

注意:要在静态模式下直接从终端运行Panagram,请执行与上述交互式模式相同的步骤,但在敲击Return之前输入相关参数。例如:-p level或-a silent listen等。

显示错误

最后,你将添加一些代码,用红色显示错误信息。

打开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 是控制字符,使终端将以下文本字符串的颜色变为红色。

注意:当你运行Panagram方案(而不是Terminal方案)时,输出开头的[;m可能看起来有点别扭。这是因为Xcode控制台不支持使用控制字符来给文本输出着色。 建立并运行,这将在终端中启动Panagram。输入f代表选项,未知选项f的错误信息将以红色显示。

image.png

从哪里开始?

你可以在这里下载本教程的最终项目。

如果你将来想写更多的命令行程序,可以看看如何将stderr重定向到日志文件,也可以看看ncurses,这是一个为终端写 "GUI风格 "程序的C库。

如果你对Swift的脚本感兴趣,你也可以看看《Swift中的脚本》这篇文章,它非常棒

我希望你喜欢这个macOS上的命令行程序教程;如果你有任何问题或评论,请随时加入下面的论坛讨论!


通过www.DeepL.com/Translator(免费版)翻译