运行 shell 命令 | 解析命令行参数 | 处理文件和目录
Swift 5.1 - 5.3 | Swift 4 | Swift 3 | Swift 2
SwiftShell
Swift 创建命令行应用程序并在 Swift 中运行 shell 命令的库。
特性
- 运行命令,并处理输出。
- 异步运行命令,并在输出可用时通知。
- 访问应用程序运行的上下文,例如环境变量、标准输入、标准输出、标准错误、当前目录和命令行参数。
- 创建您可以在其中运行命令的新上下文。
- 处理错误。
- 读取和写入文件。
参考
- API 文档.
- 项目描述,在 skilled.io。
目录
示例
打印行号
#!/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
}
您可以创建 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
中的 ReadableStream
和 WritableStream
协议可以读取和写入文本到命令、文件或 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
所有上下文(CustomContext
和 main
)实现 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
您可以处理标准输出和标准错误,并可选择等待其完成并处理任何错误。
如果您读取了命令的stderror
或stdout
,它将自动等待命令关闭其流(并可能完成运行)。您仍然可以调用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