-
-
Save andreacipriani/8c3af3719da31c8fae2cdfa8c21e17ba to your computer and use it in GitHub Desktop.
import UIKit | |
protocol CommandExecuting { | |
func run(commandName: String, arguments: [String]) throws -> String | |
} | |
enum BashError: Error { | |
case commandNotFound(name: String) | |
} | |
struct Bash: CommandExecuting { | |
func run(commandName: String, arguments: [String] = []) throws -> String { | |
return try run(resolve(commandName), with: arguments) | |
} | |
private func resolve(_ command: String) throws -> String { | |
guard var bashCommand = try? run("/bin/bash" , with: ["-l", "-c", "which \(command)"]) else { | |
throw BashError.commandNotFound(name: command) | |
} | |
bashCommand = bashCommand.trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines) | |
return bashCommand | |
} | |
private func run(_ command: String, with arguments: [String] = []) throws -> String { | |
let process = Process() | |
process.launchPath = command | |
process.arguments = arguments | |
let outputPipe = Pipe() | |
process.standardOutput = outputPipe | |
process.launch() | |
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() | |
let output = String(decoding: outputData, as: UTF8.self) | |
return output | |
} | |
} | |
let bash: CommandExecuting = Bash() | |
if let lsOutput = try? bash.run(commandName: "ls", arguments: []) { print(lsOutput) } | |
if let lsWithArgumentsOutput = try? bash.run(commandName: "ls", arguments: ["-la"]) { print(lsWithArgumentsOutput) } |
andreacipriani
commented
Aug 19, 2019
Really intrested in adding additional commands. For example I need to run the command with 2 different arguments.
example
//path -t "message" -z "message"
this has to be in 1 line but can't get to work. Any help would be a life saver.
if let lsOutput = bash.execute(commandName: "ls") { print(lsOutput) }
if let lsWithArgumentsOutput = bash.execute(commandName: "ls", arguments: ["-la"]) { print(lsWithArgumentsOutput) }
I receive "Statements are not allowed at the top level" for these two lines.
suggestion change to run(),a modal way
private func execute(command: String, arguments: [String] = []) -> String? {
let process = Process()
process.launchPath = command
process.arguments = arguments
let pipe = Pipe()
process.standardOutput = pipe
if let _ = try? process.run(){
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: String.Encoding.utf8)
return output
}else{
return nil
}
}
I believe try process.run()
at line 23 should be guard (try? process.run()) != nil else { return nil }
, otherwise error: errors thrown from here are not handled
.
Just discovered this, I have another take on this using new dynamic Swift features which would turn something like this:
let bash: CommandExecuting = Bash()
let files = bash.run(commandName: "ls", arguments: ["-la", "~/Desktop"])
into:
let bash = Command()
let files = bash.ls(
la: "~/Desktop"
)
https://gist.github.com/gabrieloc/a27e27434ea01f6c58e24faaad344836
IMHO, Swift's dynamicMember
syntactic sugar blurs the line between compile-time failures and runtime failures. If you introduce the bash.ls()
syntax vs. bash.run(commandName: "ls")
syntax, you are kind of suggesting that the ls
command is a built-in API command that is not subject to the runtime command not found
error. I wouldn't use dynamicMember
in an interface without a good reason (but that's just me :) ).
With my implementation, ls() will not autocomplete if working inside Xcode, and passing it as a string argument is just doing the same thing but with more steps. Unless you define a symbol, there’s no way to avoid runtime errors.
I wouldn’t use dynamicMember in production code either, but for scripting it minimizes all the boilerplate we’re used to autocompleting away
Also, perhaps it would be beneficial to factor out which
apart from run
to allow a consumer to decide what exactly to execute:: https://gist.github.com/gary17/b6c8b662da36cefee7010cd4bdcb2ee6
This works great, thank you for the gist!
I do get an error when using the command, although the given command is still executed. I get /bin/bash: /etc/profile: Operation not permitted
before every print out of my original command.
Example code:
let cli = Bash()
if let output = try? cli.run(commandName: "pwd") {
print(output)
}
I did change the import from UiKit
to AppKit
since I'm developing for MacOS.