SwiftShell 5.1.0

SwiftShell 5.1.0

测试已测试
语言语言 SwiftSwift
许可 MIT
发布最新发布2020 年 9 月
SPM支持 SPM

Kare Morstolkareman 维护。



  • Kare Morstol

运行 shell 命令 | 解析命令行参数 | 处理文件和目录


Swift 5.1 - 5.3 | Swift 4 | Swift 3 | Swift 2

SwiftShell logo

Platforms Swift Package Manager Carthage compatible Twitter: @nottoobadsw

SwiftShell

Swift 创建命令行应用程序并在 Swift 中运行 shell 命令的库。

特性

  • 运行命令,并处理输出。
  • 异步运行命令,并在输出可用时通知。
  • 访问应用程序运行的上下文,例如环境变量、标准输入、标准输出、标准错误、当前目录和命令行参数。
  • 创建您可以在其中运行命令的新上下文。
  • 处理错误。
  • 读取和写入文件。

参考

目录

示例

打印行号

#!/usr/bin/env swiftshell

import SwiftShell

do {
	// If there is an argument, try opening it as a file. Otherwise use standard input.
	let input = try main.arguments.first.map {try open($0)} ?? main.stdin

	input.lines().enumerated().forEach { (linenr,line) in 
		print(linenr+1, ":", line) 
	}

	// Add a newline at the end.
	print("")
} catch {
	exit(error)
}

例如,使用 cat long.txt | print_linenumbers.swiftprint_linenumbers.swift long.txt 启动,这将打印每行的行号。

其他

上下文

你在 SwiftShell 中运行的所有命令(又名进程)都需要上下文:环境变量、当前工作目录、标准输入、标准输出和标准错误(标准流)。

public struct CustomContext: Context, CommandRunning {
	public var env: [String: String]
	public var currentdirectory: String
	public var stdin: ReadableStream
	public var stdout: WritableStream
	public var stderror: WritableStream
}

您可以创建 application 的上下文副本:let context = CustomContext(main),也可以创建一个新的空上下文:let context = CustomContext()。所有内容都是可变的,因此您可以设置当前目录或将标准错误重定向到文件。

主上下文

全局变量 main 是 application 自身的上下文。除了上面提到的属性外,还有以下属性

  • public var encoding: String.Encoding 打开文件或创建新流时使用的默认编码。
  • public let tempdirectory: String 可以用于临时内容的临时目录。
  • public let arguments: [String] 启动 application 时使用的参数。
  • public let path: String application 的路径。

main.stdout 用于正常输出,例如 Swift 的 print 函数。 main.stderror 用于错误输出,而 main.stdin 是终端中提供给 application 的标准输入,类似于 somecommand | yourapplication

命令无法更改它们的上下文或 application 内部任何其他内容,因此例如 main.run("cd", "somedirectory") 将不会产生任何效果。请使用 main.currentdirectory = "somedirectory" 代替,这将改变整个 application 的当前工作目录。

示例

为终端中类似于新 macOS 用户账户环境的环境准备上下文(来自 kareman/testcommit

import SwiftShell
import Foundation

extension Dictionary where Key:Hashable {
	public func filterToDictionary <C: Collection> (keys: C) -> [Key:Value]
		where C.Iterator.Element == Key, C.IndexDistance == Int {

		var result = [Key:Value](minimumCapacity: keys.count)
		for key in keys { result[key] = self[key] }
		return result
	}
}

// Prepare an environment as close to a new OS X user account as possible.
var cleanctx = CustomContext(main)
let cleanenvvars = ["TERM_PROGRAM", "SHELL", "TERM", "TMPDIR", "Apple_PubSub_Socket_Render", "TERM_PROGRAM_VERSION", "TERM_SESSION_ID", "USER", "SSH_AUTH_SOCK", "__CF_USER_TEXT_ENCODING", "XPC_FLAGS", "XPC_SERVICE_NAME", "SHLVL", "HOME", "LOGNAME", "LC_CTYPE", "_"]
cleanctx.env = cleanctx.env.filterToDictionary(keys: cleanenvvars)
cleanctx.env["PATH"] = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"

// Create a temporary directory for testing.
cleanctx.currentdirectory = main.tempdirectory

上面提到的 Context 中的 ReadableStreamWritableStream 协议可以读取和写入文本到命令、文件或 application 的标准流。它们都有在编码/解码文本时使用的 .encoding 属性。

您可以通过 let (input,output) = streams() 创建一个新的流对。您可以在 input 中写入的内容从 output 中读取。

可写流

向可写流写入时,通常使用 .print,它正好像 Swift 内置的 print 函数一样工作。

main.stdout.print("everything is fine")
main.stderror.print("no wait, something went wrong ...")

let writefile = try open(forWriting: path) // WritableStream
writefile.print("1", 2, 3/5, separator: "+", terminator: "=")

如果您希望内容被字面理解,请使用 .write。它不会添加换行符,并且只会精确地写出您写入的内容

writefile.write("Read my lips:")

您可以关闭流,这样任何试图从另一端读取的人就不需要永远等待

writefile.close()

ReadableStream

从ReadableStream读取时,您可以一次性读取所有内容

let readfile = try open(path) // ReadableStream
let contents = readfile.read()

这将读取所有内容,并等待流关闭(如果尚未关闭)。

您还可以异步读取,即读取当前的内容并继续执行,而无需等待它关闭

while let text = main.stdin.readSome() {
	// do something with ‘text’...
}

.readSome() 返回 String? - 如果有内容则返回,如果流已关闭则返回 nil,如果没有内容且流仍打开,将等待直到有更多内容或流关闭。

另一种异步读取方式是使用 lines 方法,该方法创建一个包含流中每一行的懒惰字符串序列

for line in main.stdin.lines() {
	// ...
}

或者,您可以直接在流中获取任何数据时被通知

main.stdin.onOutput { stream in
	// ‘stream’ refers to main.stdin
}

Data

除了文本,流还可以处理原始数据

let data = Data(...)
writer.write(data: data)
reader.readSomeData()
reader.readData() 

Commands

所有上下文(CustomContextmain)实现 CommandRunning,这意味着它们可以使用自己作为上下文来运行命令。ReadableStream 和 String 也可以运行命令,它们使用 main 作为上下文,自身作为 .stdin。作为快捷方式,您可以使用 run(...) 而不是 main.run(...)

运行命令有 4 种不同的方式

Run

最简单的是直接运行命令,等待其完成并返回结果

let result1 = run("/usr/bin/executable", "argument1", "argument2")
let result2 = run("executable", "argument1", "argument2")

如果您不提供可执行文件的完整路径,则 SwiftShell 将尝试在 PATH 环境变量中的任何目录中查找它。

代码run返回以下信息:

/// Output from a `run` command.
public final class RunOutput {

	/// The error from running the command, if any.
	let error: CommandError?

	/// Standard output, trimmed for whitespace and newline if it is single-line.
	let stdout: String

	/// Standard error, trimmed for whitespace and newline if it is single-line.
	let stderror: String

	/// The exit code of the command. Anything but 0 means there was an error.
	let exitcode: Int

	/// Checks if the exit code is 0.
	let succeeded: Bool
}

例如

let date = run("date", "-u").stdout
print("Today's date in UTC is " + date)

打印输出

try runAndPrint("executable", "arg") 

这将运行类似终端中的命令,任何输出都会发送到上下文(在本例中为main)的.stdout.stderror。如果找不到可执行文件、无法访问或不可执行,或者命令返回的不是零的退出代码,那么runAndPrint将抛出CommandError

名字可能有些繁琐,但它确切地说明了它的功能。SwiftShell从不打印任何没有明确指示的内容。

异步

let command = runAsync("cmd", "-n", 245).onCompletion { command in
	// be notified when the command is finished.
}
command.stdout.onOutput { stdout in 
	// be notified when the command produces output (only on macOS).	
}

// do something with ‘command’ while it is still running.

try command.finish() // wait for it to finish.

runAsync启动一个命令并在完成之前继续。它返回一个包含此功能的AsyncCommand

    public let stdout: ReadableStream
    public let stderror: ReadableStream

    /// Is the command still running?
    public var isRunning: Bool { get }

    /// Terminates the command by sending the SIGTERM signal.
    public func stop()

    /// Interrupts the command by sending the SIGINT signal.
    public func interrupt()

    /// Temporarily suspends a command. Call resume() to resume a suspended command.
    public func suspend() -> Bool

    /// Resumes a command previously suspended with suspend().
    public func resume() -> Bool

    /// Waits for this command to finish.
    public func finish() throws -> Self

    /// Waits for command to finish, then returns with exit code.
    public func exitcode() -> Int

    /// Waits for the command to finish, then returns why the command terminated.
    /// - returns: `.exited` if the command exited normally, otherwise `.uncaughtSignal`.
    public func terminationReason() -> Process.TerminationReason

    /// Takes a closure to be called when the command has finished.
    public func onCompletion(_ handler: @escaping (AsyncCommand) -> Void) -> Self

您可以处理标准输出和标准错误,并可选择等待其完成并处理任何错误。

如果您读取了命令的stderrorstdout,它将自动等待命令关闭其流(并可能完成运行)。您仍然可以调用finish()来检查错误。

runAsyncAndPrint的功能与runAsync相同,但它会直接打印任何输出,而且它的返回类型PrintedAsyncCommand没有.stdout.stderror属性。

参数

上面的run*函数接收两种类型的参数

(_ 可执行字符串:String, _ 参数:Any ...)

如果有路径没有包含任何/,SwiftShell将尝试使用which shell命令来查找完整的路径,该命令将按照PATH环境变量中的目录顺序搜索。

参数数组可以包含任何类型,因为所有一切都是可以在Swift中转换为字符串的。如果它包含任何数组,它将展开,只使用元素而不是数组本身。

try runAndPrint("echo", "We are", 4, "arguments")
// echo "We are" 4 arguments

let array = ["But", "we", "are"]
try runAndPrint("echo", array, array.count + 2, "arguments")
// echo But we are 5 arguments
(bash bashcommand: 字符串)

这些是您通常在终端中使用的命令。您可以使用管道和重定向等所有这些好东西

try runAndPrint(bash: "cmd1 arg1 | cmd2 > output.txt")

注意,您也可以在纯SwiftShell中实现相同的功能,但远没有这么简洁

var file = try open(forWriting: "output.txt")
runAsync("cmd1", "arg1").stdout.runAsync("cmd2").stdout.write(to: &file)

错误

如果提供给runAsync的命令由于任何原因无法启动,程序将错误打印到标准错误并退出,这与脚本中通常的做法一样。如果命令的退出码不是0,则runAsync("cmd").finish()方法会引发错误

let someCommand = runAsync("cmd", "-n", 245)
// ...
do {
	try someCommand.finish()
} catch let CommandError.returnedErrorCode(command, errorcode) {
	print("Command '\(command)' finished with exit code \(errorcode).")
}

runAndPrint命令也会引发这个错误,如果命令无法启动的话

} catch CommandError.inAccessibleExecutable(let path) {
	// ‘path’ is the full path to the executable
}

您可以直接打印这些错误值值,而不是处理这些错误值

} catch {
	print(error)
}

... 或者如果它们足够严重,您可以将它们打印到标准错误并退出

} catch {
	exit(error)
}

在顶层代码级别时,您不需要捕获任何错误,但您仍然必须使用try

配置

独立项目

如果您将Misc/swiftshell-init放置在您的$PATH中的某个位置,可以使用swiftshell-init <name>创建一个新的项目。这将创建一个新的文件夹,初始化Swift Package Manager可执行文件夹结构,下载最新的SwiftShell版本,创建一个Xcode项目并打开它。运行swift build之后,您可以在.build/debug/<name>中找到编译后的可执行文件。

使用 Marathon 的脚本文件

首先将 SwiftShell 添加到 Marathon 中

marathon add https://github.com/kareman/SwiftShell.git

然后使用 marathon run <name>.swift 运行您的 Swift 脚本。或者在每个脚本文件顶部添加 #!/usr/bin/env marathon run 并使用 ./<name>.swift 运行它们。

Swift Package Manager

.package(url: "https://github.com/kareman/SwiftShell", from: "5.1.0") 添加到您的 Package.swift 文件中

// swift-tools-version:5.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "ProjectName",
    platforms: [.macOS(.v10_13)],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        .package(url: "https://github.com/kareman/SwiftShell", from: "5.1.0")
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages which this package depends on.
        .target(
            name: "ProjectName",
            dependencies: ["SwiftShell"]),
    ]
)

然后运行 swift build

Carthage

github "kareman/SwiftShell" >= 5.1 添加到您的 Cartfile 文件中,然后运行 carthage update 将生成的框架添加到应用的“嵌入的二进制文件”部分。请见 Carthage 的 README 文档 获取更多说明。

CocoaPods

SwiftShell 添加到您的 Podfile 文件中。

pod 'SwiftShell', '>= 5.1.0'

然后运行 pod install 以安装它。

许可证

遵照 MIT 许可证(MIT),https://open-source.org.cn/licenses/MIT

由 Kåre Morstøl,NotTooBad Software