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


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

SwiftShell logo

Platforms Swift Package Manager Carthage compatible Twitter: @nottoobadsw

SwiftShell

一个用于在 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
}

您可以创建应用程序上下文的副本:let context = CustomContext(main),或创建一个新的空上下文:let context = CustomContext()。一切都是可变的,因此您可以设置当前目录或将标准错误重定向到文件等。

主上下文

全局变量 main 是应用程序本身的上下文。 除了上面提到的属性之外,它还有这些

main.stdout 用于正常输出,例如 Swift 的 print 函数。 main.stderror 用于错误输出,main.stdin 是应用程序的标准输入,由终端中的 somecommand | yourapplication 之类的东西提供。

命令不能更改它们运行的上下文(或应用程序内部的任何其他内容),因此例如,main.run("cd", "somedirectory") 将不会有任何作用。 请改用 main.currentdirectory = "somedirectory",这将更改整个应用程序的当前工作目录。

示例

准备一个类似于终端中新的 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 中的协议 ReadableStream 和 WritableStream 可以从/向命令、文件或应用程序自己的标准流读取和写入文本。 它们都有一个 .encoding 属性,用于在编码/解码文本时使用。

您可以使用 let (input,output) = streams() 创建一对新的流。 您写入 input 的内容可以从 output 读取。

WritableStream

写入 WritableStream 时,通常使用 .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() 

命令

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

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

运行

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

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") 

这会像在终端中一样运行命令,其中任何输出都会分别转到 Context(在本例中为 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

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

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

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

参数

上面的 run* 函数采用 2 种不同类型的参数

(_ executable: String, _ args: 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: String)

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

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 并将生成的框架添加到应用程序的“Embedded Binaries”部分。 有关进一步的说明,请参阅 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