Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Execute shell/bash commands from Swift
protocol CommandExecuting {
func run(commandName: String, arguments: [String]) -> String?
}
struct Bash: CommandExecuting {
// MARK: - CommandExecuting
func run(commandName: String, arguments: [String] = []) -> String? {
guard var bashCommand = run(command: "/bin/bash" , arguments: ["-l", "-c", "which \(commandName)"]) else { return "\(commandName) not found" }
bashCommand = bashCommand.trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines)
return run(command: bashCommand, arguments: arguments)
}
// MARK: Private
private func run(command: String, arguments: [String] = []) -> String? {
let process = Process()
process.executableURL = URL(fileURLWithPath: command)
process.arguments = arguments
let outputPipe = Pipe()
process.standardOutput = outputPipe
try process.run()
let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
let output = String(decoding: outputData, as: UTF8.self)
return output
}
}
let bash: CommandExecuting = Bash()
if let lsOutput = bash.run(commandName: "ls", arguments: []) { print(lsOutput) }
if let lsWithArgumentsOutput = bash.run(commandName: "ls", arguments: ["-la"]) { print(lsWithArgumentsOutput) }
@0xPr0xy

This comment has been minimized.

Copy link

@0xPr0xy 0xPr0xy commented May 12, 2017

Task is renamed to Process

@andreacipriani

This comment has been minimized.

Copy link
Owner Author

@andreacipriani andreacipriani commented Dec 11, 2018

@0xPr0xy Thanks, I've updated the gist

@SSamuelsen

This comment has been minimized.

Copy link

@SSamuelsen SSamuelsen commented Dec 22, 2018

How can you execute multiple commands one after another in the same terminal window?

@VasilijSviridov

This comment has been minimized.

Copy link

@VasilijSviridov VasilijSviridov commented Mar 12, 2019

Process renamed to CommandLine

@andreacipriani

This comment has been minimized.

Copy link
Owner Author

@andreacipriani andreacipriani commented Aug 19, 2019

How can you execute multiple commands one after another in the same terminal window?
You could patch the code to take a collection of commands and use && to concatenate them?

@Elwyn1979

This comment has been minimized.

Copy link

@Elwyn1979 Elwyn1979 commented Sep 30, 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.

@vincbeaulieu

This comment has been minimized.

Copy link

@vincbeaulieu vincbeaulieu commented Dec 12, 2019

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.

@SLboat

This comment has been minimized.

Copy link

@SLboat SLboat commented Dec 27, 2019

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
        }
    }
@gary17

This comment has been minimized.

Copy link

@gary17 gary17 commented Sep 25, 2020

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.

@gabrieloc

This comment has been minimized.

Copy link

@gabrieloc gabrieloc commented Sep 30, 2020

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

@gary17

This comment has been minimized.

Copy link

@gary17 gary17 commented Sep 30, 2020

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 :) ).

@gabrieloc

This comment has been minimized.

Copy link

@gabrieloc gabrieloc commented Sep 30, 2020

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

@gary17

This comment has been minimized.

Copy link

@gary17 gary17 commented Sep 30, 2020

Also, perhaps it would be beneficial to factor out which apart from run to allow a consumer to decide what exactly to execute:

enum /* namespace */ Shell {
	static func run(command: String, arguments: [String] = []) throws -> String? {
		let process = Process()
		process.executableURL = URL(fileURLWithPath: command)
		process.arguments = arguments
		
		let outputPipe = Pipe()
		process.standardOutput = outputPipe

		try process.run()

		let output = outputPipe.fileHandleForReading.readDataToEndOfFile()
		return String(decoding: output, as: UTF8.self)
	}
}

enum Bash {
	static func which(command: String) throws -> String? {
		/*
		/bin/bash -l -c "which ls"
			expands "ls" into "/bin/ls"
		*/
		guard let pathname = try Shell.run(command: "/bin/bash" , arguments: ["-l", "-c", "which \(command)"]) else { return nil }
		return pathname.trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines)
	}
}

do {
	guard let ls = try Bash.which(command: "ls") else { fatalError("cannot locate command executable") }
	if let out = try Shell.run(command: ls, arguments: ["-la"]) { print(out) }
}
catch {
	fatalError(error.localizedDescription) // vs. exit(911)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.