运行 shell 命令 | 解析命令行参数 | 处理文件和目录
Swift 5.1 - 5.3 | Swift 4 | Swift 3 | Swift 2
一个用于在 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.swift
或 print_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
是应用程序本身的上下文。 除了上面提到的属性之外,它还有这些
public var encoding: String.Encoding
打开文件或创建新流时使用的默认编码。public let tempdirectory: String
您可以用来存放临时内容的临时目录。public let arguments: [String]
启动应用程序时使用的参数。public let path: String
应用程序的路径。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 时,通常使用 .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 读取时,您可以一次读取所有内容
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(CustomContext
和 main
)都实现了 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 种不同类型的参数
如果可执行文件的路径没有任何 /
,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
这些是您通常在终端中使用的命令。 您可以使用管道和重定向以及所有这些好东西
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>
中找到编译后的可执行文件。
首先将 SwiftShell 添加到 Marathon
marathon add https://github.com/kareman/SwiftShell.git
然后使用 marathon run <name>.swift
运行您的 Swift 脚本。 或者将 #!/usr/bin/env marathon run
添加到每个脚本文件的顶部,并使用 ./<name>.swift
运行它们。
将 .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
。
将 github "kareman/SwiftShell" >= 5.1
添加到您的 Cartfile,然后运行 carthage update
并将生成的框架添加到应用程序的“Embedded Binaries”部分。 有关进一步的说明,请参阅 Carthage 的 README。
将 SwiftShell
添加到您的 Podfile
。
pod 'SwiftShell', '>= 5.1.0'
然后运行 pod install
来安装它。
在 MIT 许可证 (MIT) 下发布,https://open-source.org.cn/licenses/MIT
Kåre Morstøl, NotTooBad Software